maplibre-preview 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +16 -0
- data/.rspec +3 -0
- data/.rubocop.yml +260 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +35 -0
- data/LICENSE +21 -0
- data/README.md +312 -0
- data/Rakefile +50 -0
- data/docs/README_RU.md +312 -0
- data/lib/maplibre-preview/public/js/contour.js +85 -0
- data/lib/maplibre-preview/public/js/filters.js +323 -0
- data/lib/maplibre-preview/version.rb +1 -1
- data/lib/maplibre-preview/views/map.slim +829 -0
- data/lib/maplibre-preview/views/map_layout.slim +329 -0
- data/lib/maplibre-preview.rb +113 -2
- data/spec/maplibre_preview_spec.rb +52 -0
- data/spec/spec_helper.rb +11 -0
- metadata +88 -35
- data/lib/maplibre-preview/sinatra_ext.rb +0 -27
- data/lib/maplibre-preview/views/maplibre_preview.slim +0 -3
@@ -0,0 +1,329 @@
|
|
1
|
+
doctype html
|
2
|
+
html
|
3
|
+
head
|
4
|
+
meta[charset="utf-8"]
|
5
|
+
title Map Preview
|
6
|
+
meta[name="viewport" content="width=device-width, initial-scale=1.0"]
|
7
|
+
link[rel="icon" type="image/png" href="/icons/favicon.png"]
|
8
|
+
link[href="https://unpkg.com/maplibre-gl@#{MapLibrePreview::MAPLIBRE_VERSION}/dist/maplibre-gl.css" rel='stylesheet']
|
9
|
+
script[src="https://unpkg.com/maplibre-gl@#{MapLibrePreview::MAPLIBRE_VERSION}/dist/maplibre-gl.js"]
|
10
|
+
script[src="https://unpkg.com/maplibre-contour@#{MapLibrePreview::CONTOUR_VERSION}/dist/index.min.js"]
|
11
|
+
script[src="https://d3js.org/d3.v#{MapLibrePreview::D3_VERSION}.min.js"]
|
12
|
+
script[src='/js/filters.js']
|
13
|
+
script[src='/js/contour.js']
|
14
|
+
style
|
15
|
+
| * { margin: 0; padding: 0; box-sizing: border-box; }
|
16
|
+
| html { height: 100%; }
|
17
|
+
| body {
|
18
|
+
| font-family: 'JetBrains Mono', 'Consolas', 'Monaco', monospace;
|
19
|
+
| background: #2b2b2b; color: #a9b7c6; line-height: 1.4; font-size: 14px;
|
20
|
+
| margin: 0; padding: 0; overflow: hidden;
|
21
|
+
| }
|
22
|
+
| .container { width: 100%; height: 100vh; margin: 0; padding: 0; }
|
23
|
+
| .header {
|
24
|
+
| background: #3c3f41; border: 1px solid #555555; padding: 20px;
|
25
|
+
| border-radius: 4px; margin-bottom: 20px;
|
26
|
+
| }
|
27
|
+
| .header h1 { color: #ffc66d; font-size: 20px; margin-bottom: 8px; }
|
28
|
+
| .header p { color: #808080; font-size: 13px; }
|
29
|
+
| .section {
|
30
|
+
| background: #3c3f41; border: 1px solid #555555; padding: 16px;
|
31
|
+
| border-radius: 4px; margin-bottom: 16px;
|
32
|
+
| }
|
33
|
+
| .section-title {
|
34
|
+
| color: #6a9955; font-weight: bold; margin-bottom: 12px; font-size: 16px;
|
35
|
+
| }
|
36
|
+
| .status-ok { color: #6a9955; }
|
37
|
+
| .status-error { color: #f44747; }
|
38
|
+
| .status-warning { color: #ffc66d; }
|
39
|
+
| .status-disabled { color: #808080; }
|
40
|
+
| .style-card {
|
41
|
+
| background: #313335; border: 1px solid #464749; padding: 12px;
|
42
|
+
| border-radius: 3px; margin-bottom: 16px;
|
43
|
+
| }
|
44
|
+
| .style-header { display: flex; justify-content: space-between; margin-bottom: 8px; }
|
45
|
+
| .style-name { color: #ffc66d; font-weight: bold; }
|
46
|
+
| .style-id { color: #808080; font-size: 12px; }
|
47
|
+
| .style-description { color: #a9b7c6; font-size: 12px; margin-bottom: 8px; }
|
48
|
+
| .style-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px; }
|
49
|
+
| .stat-item { font-size: 12px; }
|
50
|
+
| .stat-label { color: #808080; }
|
51
|
+
| .stat-value { color: #6897bb; }
|
52
|
+
| .endpoint-container {
|
53
|
+
| display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
|
54
|
+
| padding: 2px 4px; border-radius: 2px; transition: all 0.2s ease; margin-left: 4px;
|
55
|
+
| }
|
56
|
+
| .endpoint-container:hover { background: #4b4d4f; }
|
57
|
+
| .endpoint-container.copy-success {
|
58
|
+
| background: #2d4a2d !important; border: 1px solid #6a9955;
|
59
|
+
| }
|
60
|
+
| .endpoint-url {
|
61
|
+
| color: #6897bb; font-family: 'Courier New', monospace; font-size: 11px;
|
62
|
+
| text-decoration: underline;
|
63
|
+
| }
|
64
|
+
| .copy-icon {
|
65
|
+
| color: #555555; font-size: 9px; flex-shrink: 0; transition: color 0.2s; opacity: 0.5;
|
66
|
+
| }
|
67
|
+
| .endpoint-container:hover .copy-icon { color: #808080; opacity: 0.8; }
|
68
|
+
| .copy-success .copy-icon { color: #6a9955 !important; opacity: 1 !important; }
|
69
|
+
| .style-actions { margin-top: 12px; }
|
70
|
+
| .btn {
|
71
|
+
| display: inline-block; padding: 6px 12px; background: #4b4d4f; color: #a9b7c6;
|
72
|
+
| text-decoration: none; border-radius: 3px; font-size: 12px;
|
73
|
+
| border: 1px solid #555555; margin-right: 8px;
|
74
|
+
| }
|
75
|
+
| .btn:hover { background: #5a5d5f; border-color: #666666; }
|
76
|
+
| .btn-primary {
|
77
|
+
| background: #6a9955; color: #ffffff; border-color: #7bb366;
|
78
|
+
| }
|
79
|
+
| .btn-primary:hover { background: #7bb366; border-color: #8cc477; }
|
80
|
+
| .btn-small { padding: 4px 8px; font-size: 10px; margin-right: 4px; }
|
81
|
+
| .refresh-btn {
|
82
|
+
| background: #ffc66d; color: #2b2b2b; border-color: #ffd87d; font-weight: bold;
|
83
|
+
| }
|
84
|
+
| .refresh-btn:hover { background: #ffd87d; border-color: #ffe88d; }
|
85
|
+
| .header-buttons { display: flex; gap: 8px; float: right; }
|
86
|
+
| .map-btn { background: #6897bb; color: #ffffff; border-color: #7aa8cc; }
|
87
|
+
| .map-btn:hover { background: #7aa8cc; border-color: #8bb9dd; }
|
88
|
+
| .sprite-section {
|
89
|
+
| margin-top: 12px; padding-top: 12px; border-top: 1px solid #464749;
|
90
|
+
| }
|
91
|
+
| .sprite-title { color: #6a9955; font-weight: bold; font-size: 12px; margin-bottom: 8px; }
|
92
|
+
| .sprite-endpoints { display: flex; flex-direction: column; gap: 6px; }
|
93
|
+
| .sprite-item { display: flex; align-items: center; gap: 8px; font-size: 11px; }
|
94
|
+
| .sprite-label { color: #808080; min-width: 40px; }
|
95
|
+
| .stats-grid {
|
96
|
+
| display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
97
|
+
| gap: 16px; margin-bottom: 20px;
|
98
|
+
| }
|
99
|
+
| .stat-box {
|
100
|
+
| background: #313335; border: 1px solid #464749; padding: 12px;
|
101
|
+
| border-radius: 3px; text-align: center;
|
102
|
+
| }
|
103
|
+
| .stat-number { font-size: 24px; font-weight: bold; color: #6897bb; margin-bottom: 4px; }
|
104
|
+
| .stat-desc { font-size: 12px; color: #808080; }
|
105
|
+
| .error-message { color: #f44747; font-size: 12px; margin-top: 4px; }
|
106
|
+
| .success-message { color: #6a9955; font-size: 12px; margin-top: 4px; }
|
107
|
+
| .performance-overlay {
|
108
|
+
| position: fixed; top: 0; left: 50%; transform: translateX(-50%); z-index: 1000;
|
109
|
+
| background: rgba(43, 43, 43, 0.95); border: 1px solid #555555;
|
110
|
+
| border-radius: 0 0 6px 6px; padding: 0; min-width: 400px;
|
111
|
+
| backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
112
|
+
| transition: all 0.3s ease;
|
113
|
+
| }
|
114
|
+
| .performance-toggle {
|
115
|
+
| position: absolute; top: 4px; right: 4px; z-index: 1001;
|
116
|
+
| background: none; border: none; color: #808080; cursor: pointer;
|
117
|
+
| font-size: 14px; padding: 0; width: 16px; height: 16px;
|
118
|
+
| display: flex; align-items: center; justify-content: center;
|
119
|
+
| border-radius: 2px; transition: all 0.2s ease;
|
120
|
+
| }
|
121
|
+
| .performance-toggle:hover {
|
122
|
+
| background: #4b4d4f; color: #a9b7c6;
|
123
|
+
| }
|
124
|
+
| .performance-content {
|
125
|
+
| padding: 8px 12px;
|
126
|
+
| }
|
127
|
+
| .metric-row {
|
128
|
+
| display: flex; justify-content: space-between; align-items: center;
|
129
|
+
| margin-bottom: 6px;
|
130
|
+
| }
|
131
|
+
| .metric-row:last-child { margin-bottom: 0; }
|
132
|
+
| .metric {
|
133
|
+
| display: flex; align-items: center; gap: 4px;
|
134
|
+
| font-size: 11px; flex: 1; justify-content: center;
|
135
|
+
| }
|
136
|
+
| .metric-label {
|
137
|
+
| color: #808080; font-weight: 500;
|
138
|
+
| }
|
139
|
+
| .metric-value {
|
140
|
+
| color: #6897bb; font-weight: bold; font-family: 'Courier New', monospace;
|
141
|
+
| min-width: 40px; text-align: center;
|
142
|
+
| }
|
143
|
+
| .metric-value.warning { color: #ffc66d; }
|
144
|
+
| .metric-value.error { color: #f44747; }
|
145
|
+
| .metric-value.success { color: #6a9955; }
|
146
|
+
| #map-container {
|
147
|
+
| position: relative; width: 100%; height: 100vh; background: #2b2b2b;
|
148
|
+
| }
|
149
|
+
| .map-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
|
150
|
+
| .controls {
|
151
|
+
| position: absolute; top: 10px; left: 10px; z-index: 1000;
|
152
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
|
153
|
+
| padding: 10px; border-radius: 4px; color: #a9b7c6;
|
154
|
+
| }
|
155
|
+
| .controls a { color: #6897bb; text-decoration: none; margin-right: 15px; }
|
156
|
+
| .controls a:hover { text-decoration: underline; }
|
157
|
+
| .layer-controls {
|
158
|
+
| position: absolute; top: 50%; left: 10px; transform: translateY(-50%); z-index: 1000;
|
159
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; padding: 15px;
|
160
|
+
| border-radius: 4px; color: #a9b7c6; display: flex; flex-direction: column; gap: 6px;
|
161
|
+
| max-width: 300px; max-height: 80vh;
|
162
|
+
| }
|
163
|
+
| .mode-switcher {
|
164
|
+
| display: flex; gap: 2px; margin-bottom: 10px; background: #3c3f41;
|
165
|
+
| border-radius: 3px; padding: 2px; flex-shrink: 0;
|
166
|
+
| }
|
167
|
+
| .mode-button {
|
168
|
+
| background: #3c3f41; color: #808080; border: none; padding: 6px 12px;
|
169
|
+
| border-radius: 2px; cursor: pointer; font-size: 11px; transition: all 0.2s ease; flex: 1;
|
170
|
+
| }
|
171
|
+
| .mode-button:hover { background: #4b4d4f; color: #a9b7c6; }
|
172
|
+
| .mode-button.active { background: #6897bb; color: #ffffff; }
|
173
|
+
| .control-panel {
|
174
|
+
| display: none; flex-direction: column; gap: 6px;
|
175
|
+
| max-height: calc(80vh - 200px); overflow-y: auto;
|
176
|
+
| }
|
177
|
+
| .control-panel::-webkit-scrollbar { width: 8px; }
|
178
|
+
| .control-panel::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
|
179
|
+
| .control-panel::-webkit-scrollbar-thumb {
|
180
|
+
| background: #555555; border-radius: 4px; border: 1px solid #3c3f41;
|
181
|
+
| }
|
182
|
+
| .control-panel::-webkit-scrollbar-thumb:hover { background: #666666; }
|
183
|
+
| .control-panel { scrollbar-width: thin; scrollbar-color: #555555 #3c3f41; }
|
184
|
+
| .control-panel.active { display: flex; }
|
185
|
+
| .control-panel-header {
|
186
|
+
| flex-shrink: 0; border-bottom: 1px solid #555555; padding-bottom: 8px; margin-bottom: 8px;
|
187
|
+
| display: flex; flex-direction: column; gap: 6px;
|
188
|
+
| }
|
189
|
+
| .control-panel-content {
|
190
|
+
| flex: 1; overflow-y: auto;
|
191
|
+
| }
|
192
|
+
| .control-button {
|
193
|
+
| background: #4b4d4f; color: #a9b7c6; border: 1px solid #555555;
|
194
|
+
| padding: 6px 12px; border-radius: 3px; cursor: pointer; font-size: 11px;
|
195
|
+
| transition: all 0.2s ease; width: 100%; box-sizing: border-box;
|
196
|
+
| }
|
197
|
+
| .control-button:hover { background: #5a5d5f; border-color: #666666; }
|
198
|
+
| .control-button.active { background: #6a9955; color: #ffffff; border-color: #7bb366; }
|
199
|
+
| .control-button.inactive { background: #3c3f41; color: #808080; border-color: #464749; }
|
200
|
+
| .filter-group {
|
201
|
+
| margin-bottom: 8px;
|
202
|
+
| border: 1px solid #555555;
|
203
|
+
| border-radius: 3px;
|
204
|
+
| overflow: hidden;
|
205
|
+
| }
|
206
|
+
| .filter-group-button {
|
207
|
+
| width: 100%;
|
208
|
+
| font-weight: bold;
|
209
|
+
| background: #4b4d4f;
|
210
|
+
| border: none;
|
211
|
+
| border-bottom: 1px solid #555555;
|
212
|
+
| }
|
213
|
+
| .filter-sub-buttons {
|
214
|
+
| display: flex;
|
215
|
+
| flex-direction: column;
|
216
|
+
| gap: 2px;
|
217
|
+
| padding: 3px;
|
218
|
+
| background: #3c3f41;
|
219
|
+
| border: 1px solid #6a9955;
|
220
|
+
| border-radius: 2px;
|
221
|
+
| box-sizing: border-box;
|
222
|
+
| }
|
223
|
+
| .filter-sub-button {
|
224
|
+
| font-size: 10px;
|
225
|
+
| padding: 4px 8px;
|
226
|
+
| margin: 0;
|
227
|
+
| background: #ffc66d;
|
228
|
+
| color: #2b2b2b;
|
229
|
+
| border: 1px solid #e6b85a;
|
230
|
+
| border-radius: 2px;
|
231
|
+
| cursor: pointer;
|
232
|
+
| transition: all 0.2s ease;
|
233
|
+
| }
|
234
|
+
| .filter-sub-button:hover {
|
235
|
+
| background: #ffd87d;
|
236
|
+
| border-color: #f0c050;
|
237
|
+
| }
|
238
|
+
| .filter-sub-button.active {
|
239
|
+
| background: #ffd87d;
|
240
|
+
| color: #2b2b2b;
|
241
|
+
| border-color: #f0c050;
|
242
|
+
| font-weight: bold;
|
243
|
+
| }
|
244
|
+
| .filter-sub-button.inactive {
|
245
|
+
| background: #3c3f41;
|
246
|
+
| color: #808080;
|
247
|
+
| border-color: #464749;
|
248
|
+
| }
|
249
|
+
|
250
|
+
| .toggle-basemap {
|
251
|
+
| position: absolute; top: 10px; right: 10px; z-index: 1000;
|
252
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555; padding: 10px;
|
253
|
+
| border-radius: 4px; color: #a9b7c6; border: none; cursor: pointer;
|
254
|
+
| }
|
255
|
+
| .toggle-basemap:hover { background: rgba(75, 77, 79, 0.95); }
|
256
|
+
| #loading-indicator {
|
257
|
+
| position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
258
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
|
259
|
+
| border-radius: 6px; padding: 12px 20px; z-index: 1000;
|
260
|
+
| backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
261
|
+
| transition: all 0.3s ease; display: none;
|
262
|
+
| }
|
263
|
+
| .loading-progress { text-align: center; color: #a9b7c6; }
|
264
|
+
| .loading-text { font-size: 14px; color: #a9b7c6; margin-bottom: 8px; }
|
265
|
+
| .progress-bar {
|
266
|
+
| width: 200px; height: 4px; background: #3c3f41; border-radius: 2px;
|
267
|
+
| overflow: hidden; margin: 0 auto;
|
268
|
+
| }
|
269
|
+
| .progress-fill {
|
270
|
+
| height: 100%; background: #6897bb; border-radius: 2px;
|
271
|
+
| transition: width 0.3s ease; width: 0%;
|
272
|
+
| }
|
273
|
+
| /* MapLibre Controls */
|
274
|
+
| .maplibregl-ctrl-scale, .maplibregl-ctrl-group, .maplibregl-ctrl-terrain, .maplibregl-ctrl-globe {
|
275
|
+
| background: rgba(60, 63, 65, 0.95) !important; border: 1px solid #555555 !important;
|
276
|
+
| border-radius: 4px !important; backdrop-filter: blur(10px) !important;
|
277
|
+
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
|
278
|
+
| }
|
279
|
+
| .maplibregl-ctrl-scale { color: rgb(100, 185, 61) !important; font-size: 11px !important; padding: 4px 8px !important; }
|
280
|
+
| .maplibregl-ctrl-scale-line { background: #6897bb !important; border: 1px solid #7aa8cc !important; }
|
281
|
+
| .maplibregl-ctrl-group button { background: rgba(75, 105, 61, 0.8) !important; border: 1px solid rgba(72, 139, 92, 0.4) !important; color: #ffffff !important; font-size: 14px !important; font-weight: bold !important; transition: all 0.2s ease !important; }
|
282
|
+
| .maplibregl-ctrl-group button:hover { background: rgba(255, 198, 109, 0.9) !important; border-color: #ffd87d !important; color: #2b2b2b !important; transform: scale(1.05) !important; }
|
283
|
+
| .maplibregl-ctrl-group button:active { background: rgba(106, 153, 85, 1) !important; transform: scale(0.95) !important; }
|
284
|
+
| .maplibregl-ctrl-terrain button { background: rgba(104, 151, 187, 0.8) !important; border: 1px solid #7aa8cc !important; color: #ffffff !important; font-size: 16px !important; font-weight: bold !important; transition: all 0.2s ease !important; padding: 8px !important; }
|
285
|
+
| .maplibregl-ctrl-terrain button:hover { background: rgba(104, 151, 187, 1) !important; border-color: #8bb9dd !important; transform: scale(1.1) !important; box-shadow: 0 2px 8px rgba(104, 151, 187, 0.4) !important; }
|
286
|
+
| .maplibregl-ctrl-terrain button:active { transform: scale(0.95) !important; }
|
287
|
+
| .maplibregl-ctrl-globe button { background: rgba(255, 198, 109, 0.8) !important; border: 1px solid #ffd87d !important; color: #2b2b2b !important; font-size: 16px !important; font-weight: bold !important; transition: all 0.2s ease !important; padding: 8px !important; }
|
288
|
+
| .maplibregl-ctrl-globe button:hover { background: rgba(255, 198, 109, 1) !important; border-color: #ffe88d !important; transform: scale(1.1) !important; box-shadow: 0 2px 8px rgba(255, 198, 109, 0.4) !important; }
|
289
|
+
| .maplibregl-ctrl-globe button:active { transform: scale(0.95) !important; }
|
290
|
+
| .profile-overlay {
|
291
|
+
| position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 1000;
|
292
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
|
293
|
+
| border-radius: 6px; padding: 12px; width: 70vw; max-width: 800px;
|
294
|
+
| backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
295
|
+
| }
|
296
|
+
| .profile-header {
|
297
|
+
| display: flex; justify-content: space-between; align-items: center;
|
298
|
+
| margin-bottom: 8px; border-bottom: 1px solid #555555; padding-bottom: 8px;
|
299
|
+
| }
|
300
|
+
| .profile-title { color: #ffc66d; font-weight: bold; font-size: 12px; }
|
301
|
+
| .profile-close {
|
302
|
+
| background: none; border: none; color: #808080; cursor: pointer;
|
303
|
+
| font-size: 14px; padding: 0; width: 16px; height: 16px;
|
304
|
+
| display: flex; align-items: center; justify-content: center;
|
305
|
+
| border-radius: 2px; transition: all 0.2s ease;
|
306
|
+
| }
|
307
|
+
| .profile-close:hover { background: #4b4d4f; color: #a9b7c6; }
|
308
|
+
| .profile-stats {
|
309
|
+
| display: flex; justify-content: space-between; margin-bottom: 8px;
|
310
|
+
| font-size: 10px; color: #808080;
|
311
|
+
| }
|
312
|
+
| .profile-chart { margin-top: 8px; }
|
313
|
+
| .elevation-tooltip {
|
314
|
+
| position: fixed; z-index: 1001; pointer-events: none;
|
315
|
+
| background: rgba(60, 63, 65, 0.95); border: 1px solid #555555;
|
316
|
+
| border-radius: 4px; padding: 8px 12px; font-size: 16px;
|
317
|
+
| color: #6a9955; backdrop-filter: blur(10px);
|
318
|
+
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
319
|
+
| transform: translate(10px, -50%); margin-left: 10px;
|
320
|
+
| font-weight: bold;
|
321
|
+
| }
|
322
|
+
| .profile-area { fill: #4CAF50; opacity: 0.3; }
|
323
|
+
| .profile-line { fill: none; stroke: #2196F3; stroke-width: 2; }
|
324
|
+
| .profile-tooltip-line { stroke: #ff0000; stroke-width: 1; stroke-dasharray: 3,3; }
|
325
|
+
| .profile-tooltip-circle { fill: #ff0000; r: 4; }
|
326
|
+
| .profile-tooltip-text { text-anchor: middle; fill: #ff0000; font-size: 14px; }
|
327
|
+
body
|
328
|
+
.container
|
329
|
+
== yield
|
data/lib/maplibre-preview.rb
CHANGED
@@ -1,3 +1,114 @@
|
|
1
|
-
|
2
|
-
require 'maplibre-preview/sinatra_ext'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'slim'
|
5
|
+
require 'rack'
|
6
|
+
|
7
|
+
module MapLibrePreview
|
8
|
+
# Fixed versions for guaranteed compatibility
|
9
|
+
MAPLIBRE_VERSION = '5.7.3'
|
10
|
+
CONTOUR_VERSION = '0.1.0'
|
11
|
+
D3_VERSION = '7'
|
12
|
+
|
13
|
+
# Rack middleware for serving static JS files from gem
|
14
|
+
class StaticMiddleware
|
15
|
+
def initialize(app)
|
16
|
+
@app = app
|
17
|
+
@gem_public_path = File.expand_path('maplibre-preview/public', __dir__)
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
request = Rack::Request.new(env)
|
22
|
+
|
23
|
+
if request.path.match?(%r{^/js/})
|
24
|
+
serve_js_file(request.path)
|
25
|
+
else
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def serve_js_file(path)
|
33
|
+
file_path = File.join(@gem_public_path, path)
|
34
|
+
|
35
|
+
if File.exist?(file_path) && File.file?(file_path)
|
36
|
+
[200, {'Content-Type' => 'application/javascript'}, [File.read(file_path)]]
|
37
|
+
else
|
38
|
+
[404, {'Content-Type' => 'text/plain'}, ['File not found']]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Sinatra extension for map development tools
|
44
|
+
module Extension
|
45
|
+
def self.registered(app)
|
46
|
+
app.helpers Helpers
|
47
|
+
app.use StaticMiddleware
|
48
|
+
app.set :maplibre_preview_options, {}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Helper methods for map development tools
|
53
|
+
module Helpers
|
54
|
+
def render_maplibre_preview
|
55
|
+
gem_views_path = File.expand_path('maplibre-preview/views', __dir__)
|
56
|
+
original_views = settings.views
|
57
|
+
settings.set :views, gem_views_path
|
58
|
+
slim(:map, layout: :map_layout)
|
59
|
+
ensure
|
60
|
+
settings.set :views, original_views
|
61
|
+
end
|
62
|
+
|
63
|
+
def render_map_layout
|
64
|
+
gem_views_path = File.expand_path('maplibre-preview/views', __dir__)
|
65
|
+
original_views = settings.views
|
66
|
+
|
67
|
+
settings.set :views, gem_views_path
|
68
|
+
slim(:map_layout)
|
69
|
+
ensure
|
70
|
+
settings.set :views, original_views
|
71
|
+
end
|
72
|
+
|
73
|
+
def style_url
|
74
|
+
params[:style_url]
|
75
|
+
end
|
76
|
+
|
77
|
+
def should_show_map?
|
78
|
+
!!(params[:style] || params[:style_url] || params[:source])
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Standalone Sinatra application for map development
|
83
|
+
class App < Sinatra::Base
|
84
|
+
register Extension
|
85
|
+
|
86
|
+
configure do
|
87
|
+
set :views, File.expand_path('maplibre-preview/views', __dir__)
|
88
|
+
set :public_folder, File.expand_path('maplibre-preview/public', __dir__)
|
89
|
+
end
|
90
|
+
|
91
|
+
get '/js/:file' do
|
92
|
+
serve_js_file(params[:file])
|
93
|
+
end
|
94
|
+
|
95
|
+
get '/map' do
|
96
|
+
slim :map, layout: :map_layout, locals: { options: {} }
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def serve_js_file(filename)
|
102
|
+
gem_js_path = File.expand_path("maplibre-preview/public/js/#{filename}", __dir__)
|
103
|
+
if File.exist?(gem_js_path)
|
104
|
+
content_type 'application/javascript'
|
105
|
+
File.read(gem_js_path)
|
106
|
+
else
|
107
|
+
status 404
|
108
|
+
'File not found'
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
Sinatra.register MapLibrePreview::Extension
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
RSpec.describe MapLibrePreview do
|
6
|
+
describe 'App functionality' do
|
7
|
+
let(:app) { Class.new(MapLibrePreview::App) { set :environment, :test } }
|
8
|
+
|
9
|
+
it 'provides complete map development interface' do
|
10
|
+
get '/map'
|
11
|
+
expect(last_response).to be_ok
|
12
|
+
|
13
|
+
expect(last_response.body).to include('map-container')
|
14
|
+
expect(last_response.body).to include('maplibre-gl')
|
15
|
+
expect(last_response.body).to include('maplibre-contour')
|
16
|
+
expect(last_response.body).to include('d3')
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'serves all required JavaScript modules' do
|
20
|
+
%w[filters.js contour.js].each do |js_file|
|
21
|
+
get "/js/#{js_file}"
|
22
|
+
expect(last_response).to be_ok
|
23
|
+
expect(last_response.content_type).to include('javascript')
|
24
|
+
expect(last_response.body).not_to be_empty
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'External dependencies' do
|
30
|
+
let(:app) { Class.new(MapLibrePreview::App) { set :environment, :test } }
|
31
|
+
|
32
|
+
it 'includes proper external dependencies' do
|
33
|
+
get '/map'
|
34
|
+
body = last_response.body
|
35
|
+
|
36
|
+
expect(body).to include('unpkg.com/maplibre-gl')
|
37
|
+
expect(body).to include('unpkg.com/maplibre-contour')
|
38
|
+
expect(body).to include('d3js.org/d3')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'Extension integration' do
|
43
|
+
it 'provides extension for Sinatra integration' do
|
44
|
+
test_app = Class.new(Sinatra::Base) do
|
45
|
+
register MapLibrePreview::Extension
|
46
|
+
end
|
47
|
+
|
48
|
+
expect(test_app.ancestors).to include(MapLibrePreview::Extension)
|
49
|
+
expect(test_app.settings.maplibre_preview_options).to be_a(Hash)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/spec_helper.rb
ADDED