gauthamnairy commited on
Commit
0784d15
·
verified ·
1 Parent(s): b7b42d6

Upload 6 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ frontend/static/images/landing_back.jpg filter=lfs diff=lfs merge=lfs -text
frontend/elevation.html ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Ground Elevation Calculator - GeoTools Hub</title>
7
+
8
+ <!-- Leaflet CSS -->
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
10
+
11
+ <!-- Font Awesome -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
13
+
14
+ <!-- Google Fonts -->
15
+ <link rel="preconnect" href="https://fonts.googleapis.com">
16
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
17
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@700;800;900&display=swap" rel="stylesheet">
18
+
19
+ <!-- Custom CSS -->
20
+ <link rel="stylesheet" href="styles.css">
21
+ </head>
22
+ <body>
23
+ <div class="app-container">
24
+ <!-- Header -->
25
+ <header>
26
+ <div class="header-content">
27
+ <div class="title-section">
28
+ <h1 class="main-title">Ground Elevation Calculator</h1>
29
+ <p class="subtitle">Powered by GeoTools Hub</p>
30
+ </div>
31
+ <a href="index.html" class="nav-link home-btn"><i class="fas fa-home"></i> Home</a>
32
+ </div>
33
+ </header>
34
+
35
+ <!-- Main Content -->
36
+ <main>
37
+ <div class="content-wrapper">
38
+ <!-- Map Panel -->
39
+ <div class="map-panel">
40
+ <h2 class="section-title">Interactive Map</h2>
41
+ <div id="map"></div>
42
+ </div>
43
+
44
+ <!-- Input Panel -->
45
+ <div class="input-panel">
46
+ <div class="coordinates-section">
47
+ <div class="section-header">
48
+ <h2 class="section-title">Coordinates</h2>
49
+ <button id="add-coords" class="btn-action">
50
+ <i class="fas fa-plus"></i> Add Coordinate
51
+ </button>
52
+ </div>
53
+
54
+ <div id="coordinates-container">
55
+ <!-- Initial coordinate input -->
56
+ <div class="coordinate-group" data-index="0">
57
+ <div class="coord-header">
58
+ <h3>Point 1</h3>
59
+ <button class="remove-coord" title="Remove" disabled>
60
+ <i class="fas fa-times"></i>
61
+ </button>
62
+ </div>
63
+ <div class="form-row">
64
+ <div class="form-group">
65
+ <label for="latitude-0">Latitude</label>
66
+ <input type="number" id="latitude-0" class="latitude-input" step="any" placeholder="e.g. 37.7749">
67
+ </div>
68
+ <div class="form-group">
69
+ <label for="longitude-0">Longitude</label>
70
+ <input type="number" id="longitude-0" class="longitude-input" step="any" placeholder="e.g. -122.4194">
71
+ </div>
72
+ </div>
73
+ <div class="form-group">
74
+ <label for="crs-0">Coordinate Reference System</label>
75
+ <select id="crs-0" class="crs-select">
76
+ <!-- Will be populated by JavaScript -->
77
+ </select>
78
+ </div>
79
+ <div class="elevation-result hidden">
80
+ <div class="result-badge">
81
+ <div class="elevation-metrics">
82
+ <div class="metric">
83
+ <span class="value meters">0</span>
84
+ <span class="unit">meters</span>
85
+ </div>
86
+ <div class="metric">
87
+ <span class="value feet">0</span>
88
+ <span class="unit">feet</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ <div class="coord-detail">
93
+ <small>WGS84: <span class="wgs84-coords">--</span></small>
94
+ <small>Original: <span class="original-coords">--</span></small>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
100
+ <button id="calculate-all" class="btn-primary">
101
+ <i class="fas fa-calculator"></i> Calculate All Elevations
102
+ </button>
103
+ </div>
104
+
105
+ <!-- Results Section -->
106
+ <div id="results-container" class="hidden">
107
+ <h2 class="section-title">Results</h2>
108
+ <div id="elevation-results">
109
+ <!-- Results will be populated here by JavaScript -->
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </main>
115
+
116
+ <!-- Footer -->
117
+ <footer>
118
+ <p>&copy; 2025 GeoTools Hub | <a href="index.html">Back to Tools</a></p>
119
+ </footer>
120
+ </div>
121
+
122
+ <!-- Loading Overlay -->
123
+ <div id="loading-overlay" class="hidden">
124
+ <div class="loader"></div>
125
+ </div>
126
+
127
+ <!-- Leaflet JS -->
128
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
129
+
130
+ <!-- ESRI Leaflet plugins -->
131
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/esri-leaflet/3.0.10/esri-leaflet.js"></script>
132
+
133
+ <!-- Custom JS -->
134
+ <script src="script.js"></script>
135
+ </body>
136
+ </html>
frontend/index.html ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GeoTools Hub - All-in-One GIS Calculation Tool</title>
7
+
8
+ <!-- Font Awesome -->
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
10
+
11
+ <!-- Google Fonts -->
12
+ <link rel="preconnect" href="https://fonts.googleapis.com">
13
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
14
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@700;800;900&display=swap" rel="stylesheet">
15
+
16
+ <!-- Custom CSS -->
17
+ <link rel="stylesheet" href="landing.css">
18
+ </head>
19
+ <body>
20
+ <div class="map-background"></div>
21
+
22
+ <div class="background-text">MAP</div>
23
+
24
+ <div class="hero-container">
25
+ <header>
26
+ <div class="logo">
27
+ <i class="fas fa-globe-americas"></i>
28
+ <h1>GeoTools Hub</h1>
29
+ </div>
30
+ </header>
31
+
32
+ <main>
33
+ <div class="hero-content">
34
+ <h1 class="main-title">All-in-One GIS Calculation Tool</h1>
35
+ <p class="subtitle">Powerful geospatial analysis tools for professionals and enthusiasts</p>
36
+ <button id="open-drawer" class="btn-primary">
37
+ <i class="fas fa-th-large"></i> Explore Tools
38
+ </button>
39
+ </div>
40
+ </main>
41
+ </div>
42
+
43
+ <!-- App Drawer Overlay -->
44
+ <div id="app-drawer" class="app-drawer">
45
+ <div class="drawer-content">
46
+ <div class="drawer-header">
47
+ <h2>GIS Tools</h2>
48
+ <button id="close-drawer" class="btn-close">
49
+ <i class="fas fa-times"></i>
50
+ </button>
51
+ </div>
52
+
53
+ <div class="tools-grid">
54
+ <!-- Elevation Tool -->
55
+ <a href="elevation.html" class="tool-card">
56
+ <div class="tool-icon">
57
+ <i class="fas fa-mountain"></i>
58
+ </div>
59
+ <div class="tool-info">
60
+ <h3>Ground Elevation Finder</h3>
61
+ <p>Get precise elevation data for any location on Earth</p>
62
+ </div>
63
+ </a>
64
+
65
+ <!-- Future Tools (Coming Soon) -->
66
+ <div class="tool-card coming-soon">
67
+ <div class="tool-icon">
68
+ <i class="fas fa-route"></i>
69
+ </div>
70
+ <div class="tool-info">
71
+ <h3>Distance Calculator</h3>
72
+ <p>Coming soon</p>
73
+ </div>
74
+ </div>
75
+
76
+ <div class="tool-card coming-soon">
77
+ <div class="tool-icon">
78
+ <i class="fas fa-draw-polygon"></i>
79
+ </div>
80
+ <div class="tool-info">
81
+ <h3>Area Calculator</h3>
82
+ <p>Coming soon</p>
83
+ </div>
84
+ </div>
85
+
86
+ <div class="tool-card coming-soon">
87
+ <div class="tool-icon">
88
+ <i class="fas fa-map-marked-alt"></i>
89
+ </div>
90
+ <div class="tool-info">
91
+ <h3>Coordinate Converter</h3>
92
+ <p>Coming soon</p>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+
99
+ <footer>
100
+ <p>&copy; 2025 GeoTools Hub | Made with <i class="fas fa-heart"></i> for GIS professionals</p>
101
+ </footer>
102
+
103
+ <script>
104
+ // Simple script for app drawer functionality
105
+ document.getElementById('open-drawer').addEventListener('click', function() {
106
+ document.getElementById('app-drawer').classList.add('open');
107
+ document.body.style.overflow = 'hidden'; // Prevent scrolling when drawer is open
108
+ });
109
+
110
+ document.getElementById('close-drawer').addEventListener('click', function() {
111
+ document.getElementById('app-drawer').classList.remove('open');
112
+ document.body.style.overflow = ''; // Restore scrolling
113
+ });
114
+ </script>
115
+ </body>
116
+ </html>
frontend/landing.css ADDED
@@ -0,0 +1,432 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Color palette */
3
+ --primary-blue: #4776e6;
4
+ --primary-purple: #8e54e9;
5
+ --secondary-blue: #00c6ff;
6
+ --secondary-teal: #0072ff;
7
+ --dark-blue: #2a2d3e;
8
+ --text-color: #333;
9
+ --text-light: #fff;
10
+ --background-start: #f0f4ff;
11
+ --background-end: #ffffff;
12
+ --card-bg: #fff;
13
+ --border-color: #e1e4e8;
14
+ --shadow-color: rgba(0, 0, 0, 0.1);
15
+
16
+ /* Typography */
17
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18
+ --heading-font: 'Montserrat', var(--font-family);
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ }
26
+
27
+ body {
28
+ font-family: var(--font-family);
29
+ color: var(--text-color);
30
+ line-height: 1.6;
31
+ min-height: 100vh;
32
+ position: relative;
33
+ overflow-x: hidden;
34
+ background: linear-gradient(135deg, var(--background-start), var(--background-end));
35
+ }
36
+
37
+ /* Use the local world map image with proper opacity */
38
+ body::before {
39
+ content: '';
40
+ position: fixed;
41
+ top: 0;
42
+ left: 0;
43
+ width: 100%;
44
+ height: 100%;
45
+ background-image: url('static/images/landing_back.jpg');
46
+ background-size: cover;
47
+ background-position: center;
48
+ background-repeat: no-repeat;
49
+ opacity: 0.12; /* Low opacity to ensure text visibility */
50
+ z-index: -2;
51
+ pointer-events: none;
52
+ }
53
+
54
+ /* Add a subtle map grid pattern overlay */
55
+ body::after {
56
+ content: '';
57
+ position: fixed;
58
+ top: 0;
59
+ left: 0;
60
+ width: 100%;
61
+ height: 100%;
62
+ background-image: linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px),
63
+ linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
64
+ background-size: 50px 50px;
65
+ opacity: 0.5;
66
+ z-index: -1;
67
+ pointer-events: none;
68
+ }
69
+
70
+ /* Remove the alternative map source since we're using local image */
71
+ @media (max-width: 768px) {
72
+ body::before {
73
+ background-image: url('static/images/landing_back.jpg');
74
+ background-position: center;
75
+ }
76
+ }
77
+
78
+ .background-text {
79
+ position: absolute;
80
+ font-family: var(--heading-font);
81
+ font-weight: 900;
82
+ font-size: 30vw; /* Slightly smaller */
83
+ opacity: 0.04; /* Slightly more visible */
84
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
85
+ -webkit-background-clip: text;
86
+ background-clip: text;
87
+ -webkit-text-fill-color: transparent;
88
+ top: 50%;
89
+ left: 50%;
90
+ transform: translate(-50%, -50%);
91
+ z-index: -1;
92
+ letter-spacing: -0.05em;
93
+ white-space: nowrap;
94
+ }
95
+
96
+ .hero-container {
97
+ display: flex;
98
+ flex-direction: column;
99
+ min-height: 100vh;
100
+ padding-bottom: 80px; /* Add padding to bottom to make room for footer */
101
+ }
102
+
103
+ /* Header Styles */
104
+ header {
105
+ padding: 2rem;
106
+ display: flex;
107
+ justify-content: center;
108
+ }
109
+
110
+ .logo {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: 1rem;
114
+ }
115
+
116
+ .logo i {
117
+ font-size: 2.5rem;
118
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
119
+ -webkit-background-clip: text;
120
+ background-clip: text;
121
+ -webkit-text-fill-color: transparent;
122
+ }
123
+
124
+ .logo h1 {
125
+ font-family: var(--heading-font);
126
+ font-size: 2.5rem;
127
+ font-weight: 800;
128
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
129
+ -webkit-background-clip: text;
130
+ background-clip: text;
131
+ -webkit-text-fill-color: transparent;
132
+ }
133
+
134
+ /* Main Content Styles */
135
+ main {
136
+ flex: 1;
137
+ display: flex;
138
+ justify-content: center;
139
+ align-items: center;
140
+ padding: 0 2rem;
141
+ }
142
+
143
+ /* Add subtle floating animation to the hero content */
144
+ @keyframes float {
145
+ 0% { transform: translateY(0px); }
146
+ 50% { transform: translateY(-10px); }
147
+ 100% { transform: translateY(0px); }
148
+ }
149
+
150
+ .hero-content {
151
+ max-width: 800px;
152
+ text-align: center;
153
+ animation: fadeIn 1s ease, float 6s ease-in-out infinite;
154
+ position: relative;
155
+ z-index: 1;
156
+ }
157
+
158
+ .main-title {
159
+ font-family: var(--heading-font);
160
+ font-size: 3.5rem;
161
+ font-weight: 800;
162
+ margin-bottom: 1.5rem;
163
+ line-height: 1.2;
164
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
165
+ -webkit-background-clip: text;
166
+ background-clip: text;
167
+ -webkit-text-fill-color: transparent;
168
+ }
169
+
170
+ .subtitle {
171
+ font-size: 1.5rem;
172
+ margin-bottom: 2.5rem;
173
+ color: var(--dark-blue);
174
+ opacity: 0.8;
175
+ }
176
+
177
+ .btn-primary {
178
+ display: inline-flex;
179
+ align-items: center;
180
+ justify-content: center;
181
+ gap: 0.75rem;
182
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
183
+ color: var(--text-light);
184
+ border: none;
185
+ border-radius: 50px;
186
+ padding: 1rem 2.5rem;
187
+ font-size: 1.1rem;
188
+ font-weight: 600;
189
+ cursor: pointer;
190
+ transition: all 0.3s ease;
191
+ box-shadow: 0 4px 15px rgba(71, 118, 230, 0.3);
192
+ }
193
+
194
+ .btn-primary:hover {
195
+ transform: translateY(-5px);
196
+ box-shadow: 0 10px 20px rgba(71, 118, 230, 0.4);
197
+ }
198
+
199
+ .btn-primary:active {
200
+ transform: translateY(-2px);
201
+ }
202
+
203
+ /* App Drawer Styles */
204
+ .app-drawer {
205
+ position: fixed;
206
+ top: 0;
207
+ right: 0;
208
+ bottom: 0;
209
+ width: 100%;
210
+ max-width: 100%;
211
+ background-color: rgba(0, 0, 0, 0.5);
212
+ z-index: 1000;
213
+ display: flex;
214
+ justify-content: flex-end;
215
+ transform: translateX(100%);
216
+ transition: transform 0.4s cubic-bezier(0.4, 0.0, 0.2, 1);
217
+ visibility: hidden;
218
+ }
219
+
220
+ .app-drawer.open {
221
+ transform: translateX(0);
222
+ visibility: visible;
223
+ }
224
+
225
+ .drawer-content {
226
+ width: 90%;
227
+ max-width: 1200px;
228
+ height: 100%;
229
+ background-color: white;
230
+ padding: 2rem;
231
+ overflow-y: auto;
232
+ box-shadow: -5px 0 25px rgba(0, 0, 0, 0.15);
233
+ animation: slideIn 0.4s ease forwards;
234
+ }
235
+
236
+ .drawer-header {
237
+ display: flex;
238
+ justify-content: space-between;
239
+ align-items: center;
240
+ margin-bottom: 2rem;
241
+ padding-bottom: 1rem;
242
+ border-bottom: 1px solid var(--border-color);
243
+ }
244
+
245
+ .drawer-header h2 {
246
+ font-family: var(--heading-font);
247
+ font-size: 2rem;
248
+ font-weight: 700;
249
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
250
+ -webkit-background-clip: text;
251
+ background-clip: text;
252
+ -webkit-text-fill-color: transparent;
253
+ }
254
+
255
+ .btn-close {
256
+ background: none;
257
+ border: none;
258
+ font-size: 1.5rem;
259
+ color: var(--text-color);
260
+ cursor: pointer;
261
+ transition: transform 0.3s ease;
262
+ }
263
+
264
+ .btn-close:hover {
265
+ transform: rotate(90deg);
266
+ }
267
+
268
+ .tools-grid {
269
+ display: grid;
270
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
271
+ gap: 2rem;
272
+ }
273
+
274
+ .tool-card {
275
+ text-decoration: none;
276
+ color: inherit;
277
+ background-color: var(--card-bg);
278
+ border-radius: 16px;
279
+ box-shadow: 0 10px 25px -5px var(--shadow-color);
280
+ padding: 2rem;
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 1.5rem;
284
+ transition: all 0.3s ease;
285
+ position: relative;
286
+ overflow: hidden;
287
+ cursor: pointer;
288
+ }
289
+
290
+ .tool-card::before {
291
+ content: '';
292
+ position: absolute;
293
+ top: 0;
294
+ left: 0;
295
+ width: 100%;
296
+ height: 100%;
297
+ background-image: url('https://cdn.jsdelivr.net/npm/world-map-image@1.0.2/images/world-map.png');
298
+ background-size: 400%;
299
+ background-position: center;
300
+ opacity: 0.05;
301
+ z-index: 0;
302
+ transition: opacity 0.3s ease;
303
+ }
304
+
305
+ .tool-card:hover {
306
+ transform: translateY(-10px);
307
+ box-shadow: 0 20px 35px -10px var(--shadow-color);
308
+ }
309
+
310
+ .tool-card:hover::before {
311
+ opacity: 0.08;
312
+ }
313
+
314
+ .tool-card-content {
315
+ position: relative;
316
+ z-index: 1;
317
+ }
318
+
319
+ .tool-icon {
320
+ font-size: 3rem;
321
+ height: 80px;
322
+ width: 80px;
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: center;
326
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
327
+ color: white;
328
+ border-radius: 50%;
329
+ margin: 0 auto;
330
+ }
331
+
332
+ .tool-info {
333
+ text-align: center;
334
+ }
335
+
336
+ .tool-info h3 {
337
+ font-family: var(--heading-font);
338
+ font-size: 1.5rem;
339
+ margin-bottom: 0.5rem;
340
+ font-weight: 700;
341
+ }
342
+
343
+ .tool-info p {
344
+ color: var(--text-color);
345
+ opacity: 0.8;
346
+ }
347
+
348
+ .coming-soon {
349
+ position: relative;
350
+ opacity: 0.7;
351
+ cursor: not-allowed;
352
+ }
353
+
354
+ .coming-soon::after {
355
+ content: '';
356
+ position: absolute;
357
+ top: 0;
358
+ left: 0;
359
+ right: 0;
360
+ bottom: 0;
361
+ background-color: rgba(255, 255, 255, 0.5);
362
+ z-index: 1;
363
+ border-radius: 16px;
364
+ }
365
+
366
+ /* Footer Styles */
367
+ footer {
368
+ padding: 1rem;
369
+ text-align: center;
370
+ color: var(--text-color);
371
+ opacity: 0.8;
372
+ position: absolute;
373
+ bottom: 20px; /* Position 20px from bottom instead of using margin-top */
374
+ left: 0;
375
+ right: 0;
376
+ margin-top: -3rem; /* Negative margin to bring it up */
377
+ }
378
+
379
+ footer i {
380
+ color: #e25555;
381
+ }
382
+
383
+ /* Animations */
384
+ @keyframes fadeIn {
385
+ from { opacity: 0; transform: translateY(20px); }
386
+ to { opacity: 1; transform: translateY(0); }
387
+ }
388
+
389
+ @keyframes slideIn {
390
+ from { transform: translateX(50px); opacity: 0; }
391
+ to { transform: translateX(0); opacity: 1; }
392
+ }
393
+
394
+ /* Responsive Styles */
395
+ @media (max-width: 768px) {
396
+ .main-title {
397
+ font-size: 2.5rem;
398
+ }
399
+
400
+ .subtitle {
401
+ font-size: 1.2rem;
402
+ }
403
+
404
+ .drawer-content {
405
+ width: 100%;
406
+ padding: 1.5rem;
407
+ }
408
+
409
+ .tools-grid {
410
+ grid-template-columns: 1fr;
411
+ }
412
+
413
+ body::before {
414
+ background-size: 200% auto;
415
+ }
416
+
417
+ .background-text {
418
+ font-size: 40vw;
419
+ }
420
+ }
421
+
422
+ /* Update the map-background div to use the local image */
423
+ .map-background {
424
+ position: fixed;
425
+ width: 100%;
426
+ height: 100%;
427
+ top: 0;
428
+ left: 0;
429
+ z-index: -3;
430
+ background-image: url('static/images/landing_back.jpg');
431
+ opacity: 0;
432
+ }
frontend/script.js ADDED
@@ -0,0 +1,593 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API Configuration
2
+ const API_BASE_URL = window.location.hostname === 'localhost'
3
+ ? 'http://localhost:8000/api'
4
+ : `${window.location.protocol}//${window.location.host}/api`;
5
+
6
+ // DOM Elements
7
+ const coordinatesContainer = document.getElementById('coordinates-container');
8
+ const addCoordsBtn = document.getElementById('add-coords');
9
+ const calculateAllBtn = document.getElementById('calculate-all');
10
+ const resultsContainer = document.getElementById('results-container');
11
+ const elevationResultsDiv = document.getElementById('elevation-results');
12
+ const loadingOverlay = document.getElementById('loading-overlay');
13
+
14
+ // Global State
15
+ let map, markers = [];
16
+ const defaultMapView = [0, 0];
17
+ const defaultZoom = 2;
18
+ let coordinateCounter = 1;
19
+ let crsList = [];
20
+
21
+ // Initialize app
22
+ document.addEventListener('DOMContentLoaded', async () => {
23
+ try {
24
+ // Check if Leaflet is loaded
25
+ if (!window.L) {
26
+ throw new Error('Leaflet library failed to load');
27
+ }
28
+
29
+ // Initialize map
30
+ initializeMap();
31
+
32
+ // Load CRS list
33
+ crsList = await fetchCrsList();
34
+
35
+ // Setup event listeners
36
+ setupEventListeners();
37
+
38
+ // Hide loading overlay
39
+ showLoading(false);
40
+ } catch (error) {
41
+ console.error('Initialization error:', error);
42
+ showNotification(`Initialization failed: ${error.message}`, 'error');
43
+ showLoading(false);
44
+ }
45
+ });
46
+
47
+ // Initialize map with custom controls
48
+ function initializeMap() {
49
+ // Create map
50
+ map = L.map('map').setView(defaultMapView, defaultZoom);
51
+
52
+ // Add satellite imagery
53
+ L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
54
+ attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
55
+ }).addTo(map);
56
+
57
+ // Add custom reset map button
58
+ const resetControl = L.control({position: 'topright'});
59
+ resetControl.onAdd = function() {
60
+ const div = L.DomUtil.create('div', 'map-controls');
61
+ div.innerHTML = '<button id="reset-map-btn" class="reset-map-btn" title="Reset Map"><i class="fas fa-sync-alt"></i></button>';
62
+ return div;
63
+ };
64
+ resetControl.addTo(map);
65
+
66
+ // Map click event to capture coordinates
67
+ map.on('click', function(e) {
68
+ const lat = e.latlng.lat.toFixed(6);
69
+ const lng = e.latlng.lng.toFixed(6);
70
+
71
+ // Find first empty coordinate input or use the first one
72
+ const emptyInput = document.querySelector('.coordinate-group:not(.has-value)');
73
+ if (emptyInput) {
74
+ const latInput = emptyInput.querySelector('.latitude-input');
75
+ const lngInput = emptyInput.querySelector('.longitude-input');
76
+ latInput.value = lat;
77
+ lngInput.value = lng;
78
+ emptyInput.classList.add('has-value');
79
+ } else {
80
+ // If no empty inputs, add a new coordinate group
81
+ addCoordinateGroup();
82
+
83
+ // Fill the new group
84
+ const groups = document.querySelectorAll('.coordinate-group');
85
+ const newGroup = groups[groups.length - 1];
86
+ const latInput = newGroup.querySelector('.latitude-input');
87
+ const lngInput = newGroup.querySelector('.longitude-input');
88
+ latInput.value = lat;
89
+ lngInput.value = lng;
90
+ newGroup.classList.add('has-value');
91
+ }
92
+ });
93
+
94
+ // Add event listener for reset map button
95
+ document.addEventListener('click', function(e) {
96
+ if (e.target.id === 'reset-map-btn' || e.target.closest('#reset-map-btn')) {
97
+ resetMap();
98
+ }
99
+ });
100
+ }
101
+
102
+ // Setup event listeners
103
+ function setupEventListeners() {
104
+ // Add coordinates button
105
+ addCoordsBtn.addEventListener('click', addCoordinateGroup);
106
+
107
+ // Calculate all button
108
+ calculateAllBtn.addEventListener('click', calculateAllElevations);
109
+
110
+ // Initial CRS dropdown
111
+ const initialSelect = document.querySelector('.crs-select');
112
+ populateCrsDropdown(initialSelect);
113
+ }
114
+
115
+ // Add new coordinate group
116
+ function addCoordinateGroup() {
117
+ const newGroupIndex = coordinateCounter++;
118
+
119
+ const newGroup = document.createElement('div');
120
+ newGroup.className = 'coordinate-group';
121
+ newGroup.dataset.index = newGroupIndex;
122
+
123
+ newGroup.innerHTML = `
124
+ <div class="coord-header">
125
+ <h3>Point ${coordinateCounter}</h3>
126
+ <button class="remove-coord" title="Remove">
127
+ <i class="fas fa-times"></i>
128
+ </button>
129
+ </div>
130
+ <div class="form-row">
131
+ <div class="form-group">
132
+ <label for="latitude-${newGroupIndex}">Latitude</label>
133
+ <input type="number" id="latitude-${newGroupIndex}" class="latitude-input" step="any" placeholder="e.g. 37.7749">
134
+ </div>
135
+ <div class="form-group">
136
+ <label for="longitude-${newGroupIndex}">Longitude</label>
137
+ <input type="number" id="longitude-${newGroupIndex}" class="longitude-input" step="any" placeholder="e.g. -122.4194">
138
+ </div>
139
+ </div>
140
+ <div class="form-group">
141
+ <label for="crs-${newGroupIndex}">Coordinate Reference System</label>
142
+ <select id="crs-${newGroupIndex}" class="crs-select">
143
+ <!-- Will be populated by JavaScript -->
144
+ </select>
145
+ </div>
146
+ <div class="elevation-result hidden">
147
+ <div class="result-badge">
148
+ <div class="elevation-metrics">
149
+ <div class="metric">
150
+ <span class="value meters">0</span>
151
+ <span class="unit">meters</span>
152
+ </div>
153
+ <div class="metric">
154
+ <span class="value feet">0</span>
155
+ <span class="unit">feet</span>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ <div class="coord-detail">
160
+ <small>WGS84: <span class="wgs84-coords">--</span></small>
161
+ <small>Original: <span class="original-coords">--</span></small>
162
+ </div>
163
+ </div>
164
+ `;
165
+
166
+ coordinatesContainer.appendChild(newGroup);
167
+
168
+ // Populate CRS dropdown
169
+ const select = newGroup.querySelector('.crs-select');
170
+ populateCrsDropdown(select);
171
+
172
+ // Add event listener for remove button
173
+ const removeBtn = newGroup.querySelector('.remove-coord');
174
+ removeBtn.addEventListener('click', function() {
175
+ removeCoordinateGroup(newGroup);
176
+ });
177
+
178
+ // Enable all remove buttons if we have more than one coordinate group
179
+ if (document.querySelectorAll('.coordinate-group').length > 1) {
180
+ document.querySelectorAll('.remove-coord').forEach(btn => {
181
+ btn.disabled = false;
182
+ });
183
+ }
184
+ }
185
+
186
+ // Remove coordinate group
187
+ function removeCoordinateGroup(group) {
188
+ // Remove the corresponding marker
189
+ const index = parseInt(group.dataset.index);
190
+ if (markers[index]) {
191
+ map.removeLayer(markers[index]);
192
+ markers[index] = null;
193
+ }
194
+
195
+ // Remove the group element
196
+ group.remove();
197
+
198
+ // Renumber the remaining groups
199
+ const groups = document.querySelectorAll('.coordinate-group');
200
+ groups.forEach((g, i) => {
201
+ g.querySelector('h3').textContent = `Point ${i + 1}`;
202
+ });
203
+
204
+ // Disable the last remove button if only one group remains
205
+ if (groups.length === 1) {
206
+ groups[0].querySelector('.remove-coord').disabled = true;
207
+ }
208
+ }
209
+
210
+ // Fetch CRS list from API
211
+ async function fetchCrsList() {
212
+ showLoading(true);
213
+ try {
214
+ const response = await fetch(`${API_BASE_URL}/crs-list`);
215
+ if (!response.ok) {
216
+ throw new Error('Failed to fetch coordinate systems');
217
+ }
218
+
219
+ const data = await response.json();
220
+
221
+ // Populate initial dropdown
222
+ const initialSelect = document.querySelector('.crs-select');
223
+ populateCrsDropdown(initialSelect);
224
+
225
+ return data;
226
+ } catch (error) {
227
+ console.error('Error fetching CRS list:', error);
228
+ showNotification('Failed to load coordinate systems', 'error');
229
+ throw error;
230
+ } finally {
231
+ showLoading(false);
232
+ }
233
+ }
234
+
235
+ // Populate CRS dropdown
236
+ function populateCrsDropdown(selectElement) {
237
+ selectElement.innerHTML = '';
238
+
239
+ // Add WGS84 as default
240
+ const defaultOption = document.createElement('option');
241
+ defaultOption.value = 'EPSG:4326';
242
+ defaultOption.textContent = 'WGS 84 (EPSG:4326)';
243
+ selectElement.appendChild(defaultOption);
244
+
245
+ // Add other CRS options
246
+ if (crsList && crsList.length) {
247
+ crsList.forEach(crs => {
248
+ if (crs.code !== 'EPSG:4326') { // Skip WGS84 since we already added it
249
+ const option = document.createElement('option');
250
+ option.value = crs.code;
251
+ option.textContent = `${crs.name} (${crs.code})`;
252
+ selectElement.appendChild(option);
253
+ }
254
+ });
255
+ }
256
+ }
257
+
258
+ // Calculate all elevations function with improved error handling
259
+ async function calculateAllElevations() {
260
+ try {
261
+ // Show loading overlay
262
+ showLoading(true);
263
+
264
+ // Get all coordinate groups
265
+ const groups = document.querySelectorAll('.coordinate-group');
266
+ if (groups.length === 0) {
267
+ throw new Error('No coordinates to calculate');
268
+ }
269
+
270
+ // Clear previous markers
271
+ markers.forEach(marker => {
272
+ if (marker) map.removeLayer(marker);
273
+ });
274
+ markers = [];
275
+
276
+ // Store all results
277
+ const results = [];
278
+
279
+ // Flag to check if we need to update results summary
280
+ let hasSuccessfulCalculations = false;
281
+
282
+ console.log(`Processing ${groups.length} coordinate groups`);
283
+
284
+ // Process each coordinate group
285
+ for (let i = 0; i < groups.length; i++) {
286
+ const group = groups[i];
287
+ const latInput = group.querySelector('.latitude-input');
288
+ const lngInput = group.querySelector('.longitude-input');
289
+ const crsSelect = group.querySelector('.crs-select');
290
+
291
+ // Skip empty inputs
292
+ if (!latInput.value || !lngInput.value) {
293
+ console.log(`Skipping point ${i+1}: empty coordinates`);
294
+ continue;
295
+ }
296
+
297
+ try {
298
+ const lat = parseFloat(latInput.value);
299
+ const lng = parseFloat(lngInput.value);
300
+ const crs = crsSelect.value;
301
+
302
+ console.log(`Processing point ${i+1}: ${lat}, ${lng} (${crs})`);
303
+
304
+ // Validate coordinates
305
+ if (isNaN(lat) || isNaN(lng)) {
306
+ throw new Error('Invalid coordinates');
307
+ }
308
+
309
+ // Get elevation data
310
+ const elevationData = await getElevation(lat, lng, crs);
311
+ console.log('Elevation API response:', elevationData);
312
+
313
+ if (!elevationData || !('elevation' in elevationData)) {
314
+ throw new Error('Invalid elevation data received');
315
+ }
316
+
317
+ // Convert elevation to feet
318
+ const elevationFeet = elevationData.elevation * 3.28084;
319
+
320
+ // Add marker to map
321
+ addMarker(
322
+ elevationData.wgs84_latitude || elevationData.latitude,
323
+ elevationData.wgs84_longitude || elevationData.longitude,
324
+ elevationData.elevation,
325
+ elevationFeet,
326
+ i
327
+ );
328
+
329
+ // Display result in the coordinate group
330
+ displayElevationResult(group, elevationData, elevationFeet);
331
+
332
+ // Add to results
333
+ results.push(elevationData);
334
+ hasSuccessfulCalculations = true;
335
+
336
+ // Mark as having a value
337
+ group.classList.add('has-value');
338
+
339
+ } catch (error) {
340
+ console.error(`Error calculating elevation for point ${i+1}:`, error);
341
+ showNotification(`Failed to calculate elevation for Point ${i+1}: ${error.message}`, 'error');
342
+
343
+ // Display error in the coordinate group
344
+ displayElevationError(group);
345
+ }
346
+ }
347
+
348
+ // Update summary if we have results
349
+ if (hasSuccessfulCalculations) {
350
+ // Display summary
351
+ displayResultsSummary(results);
352
+
353
+ // Show results container
354
+ resultsContainer.classList.remove('hidden');
355
+ } else {
356
+ resultsContainer.classList.add('hidden');
357
+ }
358
+
359
+ } catch (error) {
360
+ console.error('Error processing elevations:', error);
361
+ showNotification(`Failed to process elevations: ${error.message}`, 'error');
362
+ } finally {
363
+ showLoading(false);
364
+ }
365
+ }
366
+
367
+ // Display elevation result with improved formatting
368
+ function displayElevationResult(group, data, elevationFeet) {
369
+ const resultDiv = group.querySelector('.elevation-result');
370
+ if (!resultDiv) return;
371
+
372
+ // Get WGS84 coordinates
373
+ const wgs84Lat = data.wgs84_latitude || data.latitude;
374
+ const wgs84Lng = data.wgs84_longitude || data.longitude;
375
+
376
+ // Build the HTML content
377
+ resultDiv.innerHTML = `
378
+ <div class="result-badge">
379
+ <div class="elevation-metrics">
380
+ <div class="metric">
381
+ <span class="value meters">${data.elevation.toFixed(2)}</span>
382
+ <span class="unit">meters</span>
383
+ </div>
384
+ <div class="metric">
385
+ <span class="value feet">${elevationFeet.toFixed(2)}</span>
386
+ <span class="unit">feet</span>
387
+ </div>
388
+ </div>
389
+ </div>
390
+ <div class="coord-detail">
391
+ <small>WGS84: ${wgs84Lat.toFixed(6)}, ${wgs84Lng.toFixed(6)}</small><br>
392
+ <small>Original: ${data.latitude.toFixed(6)}, ${data.longitude.toFixed(6)} (${data.original_crs})</small>
393
+ </div>
394
+ `;
395
+
396
+ // Show the result
397
+ resultDiv.classList.remove('hidden');
398
+ }
399
+
400
+ // Display error in elevation result
401
+ function displayElevationError(group) {
402
+ const resultDiv = group.querySelector('.elevation-result');
403
+ if (!resultDiv) return;
404
+
405
+ resultDiv.innerHTML = `
406
+ <div class="result-badge error">
407
+ <div class="elevation-metrics">
408
+ <div class="metric">
409
+ <span class="value">Error</span>
410
+ <span class="unit">Failed to calculate</span>
411
+ </div>
412
+ </div>
413
+ </div>
414
+ `;
415
+
416
+ resultDiv.classList.remove('hidden');
417
+ }
418
+
419
+ // Display results summary
420
+ function displayResultsSummary(results) {
421
+ if (!results || results.length === 0) {
422
+ elevationResultsDiv.innerHTML = '<p>No valid elevation results found.</p>';
423
+ return;
424
+ }
425
+
426
+ // Calculate statistics
427
+ const elevations = results.map(r => r.elevation);
428
+ const elevationsFeet = elevations.map(e => e * 3.28084);
429
+
430
+ const max = Math.max(...elevations);
431
+ const min = Math.min(...elevations);
432
+ const avg = elevations.reduce((sum, el) => sum + el, 0) / elevations.length;
433
+
434
+ const maxFt = max * 3.28084;
435
+ const minFt = min * 3.28084;
436
+ const avgFt = avg * 3.28084;
437
+
438
+ // Create HTML content
439
+ const html = `
440
+ <div class="results-grid">
441
+ <div class="result-card">
442
+ <div class="result-label">Points</div>
443
+ <div class="result-value">${results.length}</div>
444
+ </div>
445
+ <div class="result-card">
446
+ <div class="result-label">Highest</div>
447
+ <div class="result-value">${max.toFixed(2)} m</div>
448
+ <div class="result-value-secondary">${maxFt.toFixed(2)} ft</div>
449
+ </div>
450
+ <div class="result-card">
451
+ <div class="result-label">Lowest</div>
452
+ <div class="result-value">${min.toFixed(2)} m</div>
453
+ <div class="result-value-secondary">${minFt.toFixed(2)} ft</div>
454
+ </div>
455
+ <div class="result-card">
456
+ <div class="result-label">Average</div>
457
+ <div class="result-value">${avg.toFixed(2)} m</div>
458
+ <div class="result-value-secondary">${avgFt.toFixed(2)} ft</div>
459
+ </div>
460
+ </div>
461
+ `;
462
+
463
+ elevationResultsDiv.innerHTML = html;
464
+ }
465
+
466
+ // Add marker to map
467
+ function addMarker(lat, lng, elevationMeters, elevationFeet, index) {
468
+ const marker = L.marker([lat, lng]).addTo(map);
469
+
470
+ marker.bindPopup(`
471
+ <div class="popup-content">
472
+ <strong>Point ${index + 1}</strong><br>
473
+ Lat: ${lat.toFixed(6)}<br>
474
+ Lng: ${lng.toFixed(6)}<br>
475
+ Elevation: ${elevationMeters.toFixed(2)} m / ${elevationFeet.toFixed(2)} ft
476
+ </div>
477
+ `);
478
+
479
+ markers[index] = marker;
480
+
481
+ // If this is the first valid marker, center map on it
482
+ if (markers.filter(m => m !== null).length === 1) {
483
+ map.setView([lat, lng], 10);
484
+ }
485
+ // If we have multiple markers, fit the map to include all markers
486
+ else if (markers.filter(m => m !== null).length > 1) {
487
+ const validMarkers = markers.filter(m => m !== null);
488
+ const bounds = L.featureGroup(validMarkers).getBounds();
489
+ map.fitBounds(bounds, { padding: [50, 50] });
490
+ }
491
+ }
492
+
493
+ // Reset map
494
+ function resetMap() {
495
+ // Reset map view
496
+ map.setView(defaultMapView, defaultZoom);
497
+
498
+ // Clear all markers
499
+ markers.forEach(marker => {
500
+ if (marker) map.removeLayer(marker);
501
+ });
502
+ markers = [];
503
+
504
+ // Clear input fields and results
505
+ const groups = document.querySelectorAll('.coordinate-group');
506
+ groups.forEach(group => {
507
+ group.querySelector('.latitude-input').value = '';
508
+ group.querySelector('.longitude-input').value = '';
509
+ group.querySelector('.elevation-result').classList.add('hidden');
510
+ group.classList.remove('has-value');
511
+ });
512
+
513
+ // Hide results container
514
+ resultsContainer.classList.add('hidden');
515
+ elevationResultsDiv.innerHTML = '';
516
+
517
+ showNotification('Map has been reset', 'info');
518
+ }
519
+
520
+ // Get elevation data from API
521
+ async function getElevation(latitude, longitude, crs) {
522
+ try {
523
+ const response = await fetch(`${API_BASE_URL}/elevation`, {
524
+ method: 'POST',
525
+ headers: {
526
+ 'Content-Type': 'application/json'
527
+ },
528
+ body: JSON.stringify({
529
+ latitude: latitude,
530
+ longitude: longitude,
531
+ crs: crs
532
+ })
533
+ });
534
+
535
+ if (!response.ok) {
536
+ const errorData = await response.json();
537
+ throw new Error(errorData.detail || 'Failed to get elevation data');
538
+ }
539
+
540
+ return await response.json();
541
+ } catch (error) {
542
+ console.error('Error getting elevation:', error);
543
+ throw error;
544
+ }
545
+ }
546
+
547
+ // Function to show/hide loading overlay
548
+ function showLoading(show) {
549
+ if (show) {
550
+ loadingOverlay.classList.remove('hidden');
551
+ loadingOverlay.style.display = 'flex';
552
+ } else {
553
+ loadingOverlay.classList.add('hidden');
554
+ loadingOverlay.style.display = 'none';
555
+ }
556
+ }
557
+
558
+ // Function to show notification
559
+ function showNotification(message, type = 'info') {
560
+ // Create notification element
561
+ const notification = document.createElement('div');
562
+ notification.className = `notification ${type}`;
563
+ notification.innerHTML = `
564
+ <div class="notification-content">
565
+ <i class="fas ${type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'}"></i>
566
+ <span>${message}</span>
567
+ </div>
568
+ <button class="notification-close"><i class="fas fa-times"></i></button>
569
+ `;
570
+
571
+ // Add to document
572
+ document.body.appendChild(notification);
573
+
574
+ // Add click event to close button
575
+ notification.querySelector('.notification-close').addEventListener('click', () => {
576
+ notification.remove();
577
+ });
578
+
579
+ // Auto-remove after 5 seconds
580
+ setTimeout(() => {
581
+ notification.remove();
582
+ }, 5000);
583
+
584
+ // Animate in
585
+ setTimeout(() => {
586
+ notification.classList.add('show');
587
+ }, 10);
588
+ }
589
+
590
+ // Hide loading on window load
591
+ window.onload = function() {
592
+ showLoading(false);
593
+ };
frontend/static/images/landing_back.jpg ADDED

Git LFS Details

  • SHA256: d7d342a14e49b2b310c1c9e47f7e9db7bd8a1ec8e052038611e7aa5db32ea709
  • Pointer size: 131 Bytes
  • Size of remote file: 518 kB
frontend/styles.css ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Color palette */
3
+ --primary-blue: #4776e6;
4
+ --primary-purple: #8e54e9;
5
+ --light-purple: #b48ef9;
6
+ --secondary-blue: #00c6ff;
7
+ --secondary-teal: #0072ff;
8
+ --dark-blue: #2a2d3e;
9
+ --text-color: #333;
10
+ --text-light: #fff;
11
+ --background-color: #f5f7fa;
12
+ --card-bg: #fff;
13
+ --input-bg: #f5f7fa;
14
+ --border-color: #e1e4e8;
15
+ --shadow-color: rgba(0, 0, 0, 0.1);
16
+ --success-color: #10b981;
17
+
18
+ /* Typography */
19
+ --font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
20
+ --heading-font: 'Montserrat', var(--font-family);
21
+ }
22
+
23
+ * {
24
+ margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ body {
30
+ font-family: var(--font-family);
31
+ background-color: var(--background-color);
32
+ color: var(--text-color);
33
+ line-height: 1.6;
34
+ }
35
+
36
+ .app-container {
37
+ display: flex;
38
+ flex-direction: column;
39
+ min-height: 100vh;
40
+ }
41
+
42
+ /* Updated Header Styles */
43
+ header {
44
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
45
+ color: var(--text-light);
46
+ padding: 0.9rem 2rem;
47
+ box-shadow: 0 4px 6px var(--shadow-color);
48
+ }
49
+
50
+ .header-content {
51
+ max-width: 1400px;
52
+ margin: 0 auto;
53
+ display: flex;
54
+ flex-direction: column;
55
+ justify-content: center;
56
+ align-items: center;
57
+ text-align: center;
58
+ }
59
+
60
+ .title-section {
61
+ text-align: center;
62
+ margin-bottom: 0.25rem;
63
+ }
64
+
65
+ .main-title {
66
+ font-family: var(--heading-font);
67
+ font-size: 1.7rem;
68
+ font-weight: 700;
69
+ margin-bottom: 0.15rem;
70
+ }
71
+
72
+ .subtitle {
73
+ font-size: 0.85rem;
74
+ opacity: 0.9;
75
+ }
76
+
77
+ .home-btn {
78
+ padding: 0.35rem 0.8rem;
79
+ background-color: rgba(255, 255, 255, 0.2);
80
+ border-radius: 6px;
81
+ transition: background-color 0.3s ease;
82
+ font-size: 0.9rem;
83
+ position: absolute;
84
+ right: 20px;
85
+ top: 1rem;
86
+ }
87
+
88
+ .home-btn:hover {
89
+ background-color: rgba(255, 255, 255, 0.3);
90
+ }
91
+
92
+ /* Updated Layout Styles */
93
+ main {
94
+ flex: 1;
95
+ padding: 1.5rem;
96
+ }
97
+
98
+ .content-wrapper {
99
+ display: grid;
100
+ grid-template-columns: 1fr 1fr;
101
+ gap: 1.5rem;
102
+ max-width: 1440px;
103
+ margin: 0 auto;
104
+ }
105
+
106
+ .map-panel {
107
+ background: white;
108
+ border-radius: 8px;
109
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
110
+ overflow: hidden;
111
+ position: relative;
112
+ }
113
+
114
+ #map {
115
+ height: calc(100vh - 200px);
116
+ width: 100%;
117
+ z-index: 1;
118
+ border-radius: 0 0 8px 8px;
119
+ }
120
+
121
+ .section-title {
122
+ font-family: var(--heading-font);
123
+ font-size: 1.2rem;
124
+ padding: 1rem;
125
+ margin: 0;
126
+ background-color: var(--card-bg);
127
+ border-bottom: 1px solid var(--border-color);
128
+ }
129
+
130
+ /* Custom map controls */
131
+ .map-controls {
132
+ position: absolute;
133
+ top: 70px;
134
+ right: 10px;
135
+ z-index: 1000;
136
+ }
137
+
138
+ .reset-map-btn {
139
+ background: white;
140
+ border: none;
141
+ border-radius: 4px;
142
+ width: 30px;
143
+ height: 30px;
144
+ box-shadow: 0 0 0 2px rgba(0,0,0,0.2);
145
+ cursor: pointer;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ font-size: 16px;
150
+ color: #333;
151
+ }
152
+
153
+ .reset-map-btn:hover {
154
+ background-color: #f4f4f4;
155
+ }
156
+
157
+ /* Input Panel Styles */
158
+ .input-panel {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 1.5rem;
162
+ height: 100%;
163
+ }
164
+
165
+ .coordinates-section {
166
+ background: white;
167
+ border-radius: 8px;
168
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
169
+ padding-bottom: 1rem;
170
+ }
171
+
172
+ .section-header {
173
+ display: flex;
174
+ justify-content: space-between;
175
+ align-items: center;
176
+ padding-right: 1rem;
177
+ border-bottom: 1px solid var(--border-color);
178
+ }
179
+
180
+ #coordinates-container {
181
+ padding: 1rem;
182
+ max-height: calc(100vh - 400px);
183
+ overflow-y: auto;
184
+ }
185
+
186
+ .coordinate-group {
187
+ background: var(--input-bg);
188
+ border-radius: 8px;
189
+ padding: 1rem;
190
+ margin-bottom: 1rem;
191
+ position: relative;
192
+ border-left: 3px solid var(--primary-purple);
193
+ }
194
+
195
+ .coord-header {
196
+ display: flex;
197
+ justify-content: space-between;
198
+ align-items: center;
199
+ margin-bottom: 0.75rem;
200
+ }
201
+
202
+ .coord-header h3 {
203
+ font-size: 1rem;
204
+ font-weight: 600;
205
+ margin: 0;
206
+ }
207
+
208
+ .form-row {
209
+ display: flex;
210
+ gap: 0.75rem;
211
+ margin-bottom: 0.75rem;
212
+ }
213
+
214
+ .form-group {
215
+ flex: 1;
216
+ margin-bottom: 0.75rem;
217
+ }
218
+
219
+ label {
220
+ display: block;
221
+ font-size: 0.85rem;
222
+ margin-bottom: 0.25rem;
223
+ color: var(--text-color);
224
+ opacity: 0.8;
225
+ }
226
+
227
+ input, select {
228
+ width: 100%;
229
+ padding: 0.6rem;
230
+ border: 1px solid var(--border-color);
231
+ border-radius: 4px;
232
+ font-family: inherit;
233
+ font-size: 0.9rem;
234
+ }
235
+
236
+ input:focus, select:focus {
237
+ outline: none;
238
+ border-color: var(--primary-purple);
239
+ box-shadow: 0 0 0 2px rgba(142, 84, 233, 0.2);
240
+ }
241
+
242
+ .btn-action {
243
+ background: rgba(255, 255, 255, 0.3);
244
+ color: var(--primary-purple);
245
+ border: 1px solid var(--primary-purple);
246
+ border-radius: 4px;
247
+ padding: 0.3rem 0.6rem;
248
+ font-size: 0.8rem;
249
+ cursor: pointer;
250
+ transition: all 0.3s;
251
+ }
252
+
253
+ .btn-action:hover {
254
+ background-color: rgba(142, 84, 233, 0.1);
255
+ }
256
+
257
+ .btn-primary {
258
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
259
+ color: white;
260
+ border: none;
261
+ border-radius: 4px;
262
+ padding: 0.8rem 1.2rem;
263
+ font-size: 0.95rem;
264
+ font-weight: 500;
265
+ cursor: pointer;
266
+ transition: all 0.3s;
267
+ margin: 0 1rem;
268
+ width: calc(100% - 2rem);
269
+ }
270
+
271
+ .btn-primary:hover {
272
+ background: linear-gradient(135deg, var(--primary-blue), var(--light-purple));
273
+ box-shadow: 0 4px 12px rgba(142, 84, 233, 0.2);
274
+ }
275
+
276
+ .remove-coord {
277
+ background: none;
278
+ border: none;
279
+ color: #9ca3af;
280
+ cursor: pointer;
281
+ font-size: 0.95rem;
282
+ padding: 0.2rem;
283
+ border-radius: 4px;
284
+ transition: all 0.2s;
285
+ }
286
+
287
+ .remove-coord:not([disabled]):hover {
288
+ background-color: #f3f4f6;
289
+ color: #ef4444;
290
+ }
291
+
292
+ .remove-coord[disabled] {
293
+ opacity: 0.5;
294
+ cursor: not-allowed;
295
+ }
296
+
297
+ /* Results Styles */
298
+ .elevation-result {
299
+ margin-top: 1rem;
300
+ border-top: 1px dashed #e1e4e8;
301
+ padding-top: 1rem;
302
+ }
303
+
304
+ .result-badge {
305
+ background: linear-gradient(135deg, var(--primary-blue), var(--primary-purple));
306
+ padding: 1rem;
307
+ border-radius: 8px;
308
+ color: white;
309
+ margin-bottom: 0.5rem;
310
+ }
311
+
312
+ .result-badge.error {
313
+ background: linear-gradient(135deg, #e53e3e, #d00000);
314
+ }
315
+
316
+ .elevation-metrics {
317
+ display: flex;
318
+ justify-content: space-between;
319
+ align-items: center;
320
+ }
321
+
322
+ .metric {
323
+ text-align: center;
324
+ }
325
+
326
+ .metric .value {
327
+ font-size: 1.4rem;
328
+ font-weight: 600;
329
+ display: block;
330
+ }
331
+
332
+ .metric .unit {
333
+ font-size: 0.8rem;
334
+ opacity: 0.9;
335
+ }
336
+
337
+ .coord-detail {
338
+ font-size: 0.75rem;
339
+ color: #718096;
340
+ line-height: 1.4;
341
+ }
342
+
343
+ .results-grid {
344
+ display: grid;
345
+ grid-template-columns: repeat(2, 1fr);
346
+ gap: 1rem;
347
+ margin-top: 1rem;
348
+ }
349
+
350
+ .result-card {
351
+ background-color: white;
352
+ padding: 1rem;
353
+ border-radius: 8px;
354
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
355
+ text-align: center;
356
+ }
357
+
358
+ .result-label {
359
+ font-size: 0.9rem;
360
+ color: #718096;
361
+ margin-bottom: 0.5rem;
362
+ }
363
+
364
+ .result-value {
365
+ font-size: 1.5rem;
366
+ font-weight: 600;
367
+ color: var(--primary-purple);
368
+ }
369
+
370
+ .result-value-secondary {
371
+ font-size: 0.9rem;
372
+ color: #718096;
373
+ }
374
+
375
+ /* Results Container */
376
+ #results-container {
377
+ background: white;
378
+ border-radius: 8px;
379
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
380
+ overflow: hidden;
381
+ }
382
+
383
+ .result-card {
384
+ background: var(--input-bg);
385
+ padding: 1rem;
386
+ border-radius: 8px;
387
+ text-align: center;
388
+ }
389
+
390
+ .result-label {
391
+ font-size: 0.9rem;
392
+ color: var(--text-color);
393
+ opacity: 0.7;
394
+ margin-bottom: 0.5rem;
395
+ }
396
+
397
+ .result-value {
398
+ font-size: 1.5rem;
399
+ font-weight: 600;
400
+ color: var(--primary-purple);
401
+ }
402
+
403
+ .result-value-secondary {
404
+ font-size: 0.9rem;
405
+ color: var(--primary-blue);
406
+ opacity: 0.8;
407
+ }
408
+
409
+ .dual-units {
410
+ display: flex;
411
+ flex-direction: column;
412
+ align-items: center;
413
+ gap: 0.25rem;
414
+ }
415
+
416
+ /* Utility Classes */
417
+ .hidden {
418
+ display: none !important;
419
+ }
420
+
421
+ /* Loading Overlay */
422
+ #loading-overlay {
423
+ position: fixed;
424
+ top: 0;
425
+ left: 0;
426
+ right: 0;
427
+ bottom: 0;
428
+ background-color: rgba(0, 0, 0, 0.6);
429
+ display: flex;
430
+ justify-content: center;
431
+ align-items: center;
432
+ z-index: 9999;
433
+ }
434
+
435
+ .loader {
436
+ width: 50px;
437
+ height: 50px;
438
+ border: 5px solid var(--text-light);
439
+ border-top: 5px solid var(--primary-purple);
440
+ border-radius: 50%;
441
+ animation: spin 1s linear infinite;
442
+ }
443
+
444
+ @keyframes spin {
445
+ 0% { transform: rotate(0deg); }
446
+ 100% { transform: rotate(360deg); }
447
+ }
448
+
449
+ /* Footer */
450
+ footer {
451
+ background-color: var(--dark-blue);
452
+ color: var(--text-light);
453
+ text-align: center;
454
+ padding: 1rem;
455
+ margin-top: auto;
456
+ }
457
+
458
+ footer a {
459
+ color: var(--text-light);
460
+ text-decoration: none;
461
+ }
462
+
463
+ footer a:hover {
464
+ text-decoration: underline;
465
+ }
466
+
467
+ /* Responsive Styles */
468
+ @media (max-width: 992px) {
469
+ .content-wrapper {
470
+ grid-template-columns: 1fr;
471
+ }
472
+
473
+ #map {
474
+ height: 400px;
475
+ }
476
+
477
+ #coordinates-container {
478
+ max-height: 400px;
479
+ }
480
+ }
481
+
482
+ @media (max-width: 768px) {
483
+ .home-btn {
484
+ position: static;
485
+ margin-top: 1rem;
486
+ }
487
+
488
+ .header-content {
489
+ padding: 1rem;
490
+ }
491
+
492
+ .form-row {
493
+ flex-direction: column;
494
+ gap: 0.5rem;
495
+ }
496
+
497
+ .main-title {
498
+ font-size: 1.5rem;
499
+ }
500
+
501
+ main {
502
+ padding: 1rem;
503
+ }
504
+ }