maplibre-preview 1.8.0 → 1.9.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/CHANGELOG.md +14 -0
- data/lib/maplibre-preview/public/css/temporal_picker.css +164 -0
- data/lib/maplibre-preview/public/js/temporal_picker.js +247 -0
- data/lib/maplibre-preview/version.rb +1 -1
- data/lib/maplibre-preview/views/maplibre_layout.slim +39 -5
- data/lib/maplibre-preview/views/maplibre_map.slim +151 -22
- data/spec/maplibre_preview_spec.rb +23 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2c43caaa2de2305aa8211b7d1de99aaf7337b6162d7d99a448f73007c717591a
|
|
4
|
+
data.tar.gz: b16f170dc316e26844ffa6dd44d8401d38ba9128a9cf0dc13bd3abd1b30d0507
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2401ce59b8a156a2b34ddeecc36a3c6ad4ae161f41fcf998d3e2a6c788cf096f62fde591327948bdf18f1625b8eb9d0e55f226f8d758f7d6b0d8524087bc43f1
|
|
7
|
+
data.tar.gz: ad6e8ab6f576675903edda6b0859df42ef467dffff70e651eaacb36bbb8c906806b7d3bfec2d29545bcad663bcc99a114d88b912edd22caf115d5b7ecbdada0f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.0] - 2026-05-14
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **Temporal parameter picker** - added a custom calendar and time picker for date/time-like style parameters
|
|
7
|
+
- **Style parameter context** - show source/layer counts and localized usage hints for each detected style parameter
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Style parameter URL matching** - track source-specific parameterized URL rules and append only the parameters declared for the matching source
|
|
11
|
+
- **Temporal parameter inputs** - use the custom picker for temporal parameters while keeping query values normalized to epoch seconds
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Source metadata inspection** - fetch source metadata without pre-appending parameter values so metadata-declared parameters can be discovered reliably
|
|
15
|
+
- **Parameterized tile requests** - removed the broad `/rb_tiles/` heuristic in favor of explicit source metadata and URL prefix matching
|
|
16
|
+
|
|
3
17
|
## [1.8.0] - 2026-05-14
|
|
4
18
|
|
|
5
19
|
### Added
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
.temporal-picker-popover {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1300;
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 8px;
|
|
7
|
+
background: rgba(49, 51, 53, 0.98);
|
|
8
|
+
border: 1px solid #555555;
|
|
9
|
+
border-radius: 4px;
|
|
10
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
|
|
11
|
+
color: #a9b7c6;
|
|
12
|
+
padding: 12px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.temporal-picker-header {
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.temporal-picker-title {
|
|
23
|
+
color: #ffc66d;
|
|
24
|
+
font-size: 11px;
|
|
25
|
+
font-weight: bold;
|
|
26
|
+
text-transform: uppercase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.temporal-picker-nav {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
gap: 4px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.temporal-picker-nav-button {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
justify-content: center;
|
|
38
|
+
width: 24px;
|
|
39
|
+
height: 24px;
|
|
40
|
+
background: #3c3f41;
|
|
41
|
+
color: #a9b7c6;
|
|
42
|
+
border: 1px solid #555555;
|
|
43
|
+
border-radius: 3px;
|
|
44
|
+
font-size: 16px;
|
|
45
|
+
line-height: 1;
|
|
46
|
+
cursor: pointer;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.temporal-picker-nav-button:hover {
|
|
50
|
+
background: #5a5d5f;
|
|
51
|
+
border-color: #666666;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.temporal-picker-calendar {
|
|
55
|
+
display: grid;
|
|
56
|
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
57
|
+
gap: 4px;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.temporal-picker-weekday {
|
|
61
|
+
color: #808080;
|
|
62
|
+
font-size: 10px;
|
|
63
|
+
font-weight: bold;
|
|
64
|
+
text-align: center;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.temporal-picker-day {
|
|
69
|
+
height: 28px;
|
|
70
|
+
background: transparent;
|
|
71
|
+
color: #a9b7c6;
|
|
72
|
+
border: 1px solid transparent;
|
|
73
|
+
border-radius: 3px;
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.temporal-picker-day:hover {
|
|
79
|
+
background: #4b4d4f;
|
|
80
|
+
border-color: #555555;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.temporal-picker-day.muted {
|
|
84
|
+
color: #666666;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.temporal-picker-day.today {
|
|
88
|
+
border-color: #6897bb;
|
|
89
|
+
color: #ffffff;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.temporal-picker-day.selected {
|
|
93
|
+
background: #6897bb;
|
|
94
|
+
border-color: #7aa8cc;
|
|
95
|
+
color: #ffffff;
|
|
96
|
+
font-weight: bold;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.temporal-picker-time {
|
|
100
|
+
display: grid;
|
|
101
|
+
grid-template-columns: 1fr auto 1fr;
|
|
102
|
+
align-items: center;
|
|
103
|
+
gap: 6px;
|
|
104
|
+
color: #808080;
|
|
105
|
+
font-family: 'Courier New', monospace;
|
|
106
|
+
font-size: 12px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.temporal-picker-select {
|
|
110
|
+
width: 100%;
|
|
111
|
+
background: #3c3f41;
|
|
112
|
+
color: #a9b7c6;
|
|
113
|
+
border: 1px solid #555555;
|
|
114
|
+
border-radius: 3px;
|
|
115
|
+
padding: 6px 8px;
|
|
116
|
+
font-family: 'Courier New', monospace;
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
color-scheme: dark;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.temporal-picker-select:focus {
|
|
122
|
+
outline: none;
|
|
123
|
+
border-color: #6897bb;
|
|
124
|
+
box-shadow: 0 0 0 1px #6897bb;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.temporal-picker-footer {
|
|
128
|
+
display: grid;
|
|
129
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
130
|
+
gap: 6px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.temporal-picker-button {
|
|
134
|
+
background: #3c3f41;
|
|
135
|
+
color: #a9b7c6;
|
|
136
|
+
border: 1px solid #555555;
|
|
137
|
+
border-radius: 3px;
|
|
138
|
+
padding: 6px 8px;
|
|
139
|
+
font-size: 11px;
|
|
140
|
+
font-weight: bold;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.temporal-picker-button:hover {
|
|
145
|
+
background: #5a5d5f;
|
|
146
|
+
border-color: #666666;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.temporal-picker-button:focus {
|
|
150
|
+
outline: none;
|
|
151
|
+
border-color: #6897bb;
|
|
152
|
+
box-shadow: 0 0 0 1px #6897bb;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.temporal-picker-button.primary {
|
|
156
|
+
background: #6897bb;
|
|
157
|
+
border-color: #7aa8cc;
|
|
158
|
+
color: #ffffff;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.temporal-picker-button.primary:hover {
|
|
162
|
+
background: #7aa8cc;
|
|
163
|
+
border-color: #8bb9dd;
|
|
164
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
let activePicker = null;
|
|
3
|
+
|
|
4
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
5
|
+
|
|
6
|
+
const parseInputDate = (value) => {
|
|
7
|
+
const parsed = value ? new Date(value) : new Date();
|
|
8
|
+
return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const formatInputDate = (date) => {
|
|
12
|
+
const pad = number => String(number).padStart(2, '0');
|
|
13
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const close = () => {
|
|
17
|
+
if (!activePicker) return;
|
|
18
|
+
|
|
19
|
+
activePicker.cleanup?.();
|
|
20
|
+
activePicker.element.remove();
|
|
21
|
+
activePicker = null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const position = (input, picker) => {
|
|
25
|
+
const rect = input.getBoundingClientRect();
|
|
26
|
+
const pickerWidth = Math.min(336, window.innerWidth - 16);
|
|
27
|
+
const left = clamp(rect.left, 8, window.innerWidth - pickerWidth - 8);
|
|
28
|
+
const aboveTop = rect.top - picker.offsetHeight - 8;
|
|
29
|
+
const belowTop = rect.bottom + 8;
|
|
30
|
+
const top = aboveTop >= 8 ? aboveTop : Math.min(belowTop, window.innerHeight - picker.offsetHeight - 8);
|
|
31
|
+
|
|
32
|
+
picker.style.width = `${pickerWidth}px`;
|
|
33
|
+
picker.style.left = `${left}px`;
|
|
34
|
+
picker.style.top = `${Math.max(8, top)}px`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const render = (state) => {
|
|
38
|
+
const {element, input, onApply, onClear} = state;
|
|
39
|
+
const monthDate = state.visibleMonth;
|
|
40
|
+
const selected = state.selectedDate;
|
|
41
|
+
const today = new Date();
|
|
42
|
+
const monthLabel = monthDate.toLocaleDateString('en-US', {month: 'long', year: 'numeric'});
|
|
43
|
+
const monthStart = new Date(monthDate.getFullYear(), monthDate.getMonth(), 1);
|
|
44
|
+
const firstOffset = (monthStart.getDay() + 6) % 7;
|
|
45
|
+
const firstCell = new Date(monthStart);
|
|
46
|
+
firstCell.setDate(monthStart.getDate() - firstOffset);
|
|
47
|
+
|
|
48
|
+
element.innerHTML = '';
|
|
49
|
+
|
|
50
|
+
const header = document.createElement('div');
|
|
51
|
+
header.className = 'temporal-picker-header';
|
|
52
|
+
|
|
53
|
+
const title = document.createElement('div');
|
|
54
|
+
title.className = 'temporal-picker-title';
|
|
55
|
+
title.textContent = monthLabel;
|
|
56
|
+
|
|
57
|
+
const nav = document.createElement('div');
|
|
58
|
+
nav.className = 'temporal-picker-nav';
|
|
59
|
+
[
|
|
60
|
+
['Previous month', -1, '<'],
|
|
61
|
+
['Next month', 1, '>']
|
|
62
|
+
].forEach(([label, delta, text]) => {
|
|
63
|
+
const button = document.createElement('button');
|
|
64
|
+
button.type = 'button';
|
|
65
|
+
button.className = 'temporal-picker-nav-button';
|
|
66
|
+
button.title = label;
|
|
67
|
+
button.setAttribute('aria-label', label);
|
|
68
|
+
button.textContent = text;
|
|
69
|
+
button.addEventListener('click', () => {
|
|
70
|
+
state.visibleMonth = new Date(monthDate.getFullYear(), monthDate.getMonth() + delta, 1);
|
|
71
|
+
render(state);
|
|
72
|
+
});
|
|
73
|
+
nav.appendChild(button);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
header.appendChild(title);
|
|
77
|
+
header.appendChild(nav);
|
|
78
|
+
element.appendChild(header);
|
|
79
|
+
|
|
80
|
+
const calendar = document.createElement('div');
|
|
81
|
+
calendar.className = 'temporal-picker-calendar';
|
|
82
|
+
['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'].forEach(day => {
|
|
83
|
+
const cell = document.createElement('div');
|
|
84
|
+
cell.className = 'temporal-picker-weekday';
|
|
85
|
+
cell.textContent = day;
|
|
86
|
+
calendar.appendChild(cell);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
for (let index = 0; index < 42; index += 1) {
|
|
90
|
+
const dayDate = new Date(firstCell);
|
|
91
|
+
dayDate.setDate(firstCell.getDate() + index);
|
|
92
|
+
|
|
93
|
+
const button = document.createElement('button');
|
|
94
|
+
button.type = 'button';
|
|
95
|
+
button.className = 'temporal-picker-day';
|
|
96
|
+
dayDate.getMonth() !== monthDate.getMonth() && button.classList.add('muted');
|
|
97
|
+
dayDate.toDateString() === today.toDateString() && button.classList.add('today');
|
|
98
|
+
dayDate.toDateString() === selected.toDateString() && button.classList.add('selected');
|
|
99
|
+
button.textContent = String(dayDate.getDate());
|
|
100
|
+
button.addEventListener('click', () => {
|
|
101
|
+
state.selectedDate = new Date(
|
|
102
|
+
dayDate.getFullYear(),
|
|
103
|
+
dayDate.getMonth(),
|
|
104
|
+
dayDate.getDate(),
|
|
105
|
+
selected.getHours(),
|
|
106
|
+
selected.getMinutes()
|
|
107
|
+
);
|
|
108
|
+
state.visibleMonth = new Date(dayDate.getFullYear(), dayDate.getMonth(), 1);
|
|
109
|
+
render(state);
|
|
110
|
+
});
|
|
111
|
+
calendar.appendChild(button);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
element.appendChild(calendar);
|
|
115
|
+
|
|
116
|
+
const timeRow = document.createElement('div');
|
|
117
|
+
timeRow.className = 'temporal-picker-time';
|
|
118
|
+
|
|
119
|
+
const hourSelect = document.createElement('select');
|
|
120
|
+
hourSelect.className = 'temporal-picker-select';
|
|
121
|
+
hourSelect.setAttribute('aria-label', 'Hour');
|
|
122
|
+
|
|
123
|
+
const minuteSelect = document.createElement('select');
|
|
124
|
+
minuteSelect.className = 'temporal-picker-select';
|
|
125
|
+
minuteSelect.setAttribute('aria-label', 'Minute');
|
|
126
|
+
|
|
127
|
+
for (let hour = 0; hour < 24; hour += 1) {
|
|
128
|
+
const option = document.createElement('option');
|
|
129
|
+
option.value = String(hour);
|
|
130
|
+
option.textContent = String(hour).padStart(2, '0');
|
|
131
|
+
hourSelect.appendChild(option);
|
|
132
|
+
}
|
|
133
|
+
for (let minute = 0; minute < 60; minute += 1) {
|
|
134
|
+
const option = document.createElement('option');
|
|
135
|
+
option.value = String(minute);
|
|
136
|
+
option.textContent = String(minute).padStart(2, '0');
|
|
137
|
+
minuteSelect.appendChild(option);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
hourSelect.value = String(selected.getHours());
|
|
141
|
+
minuteSelect.value = String(selected.getMinutes());
|
|
142
|
+
|
|
143
|
+
const updateTime = () => {
|
|
144
|
+
state.selectedDate = new Date(
|
|
145
|
+
state.selectedDate.getFullYear(),
|
|
146
|
+
state.selectedDate.getMonth(),
|
|
147
|
+
state.selectedDate.getDate(),
|
|
148
|
+
Number(hourSelect.value),
|
|
149
|
+
Number(minuteSelect.value)
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
hourSelect.addEventListener('change', updateTime);
|
|
154
|
+
minuteSelect.addEventListener('change', updateTime);
|
|
155
|
+
|
|
156
|
+
timeRow.appendChild(hourSelect);
|
|
157
|
+
timeRow.appendChild(document.createTextNode(':'));
|
|
158
|
+
timeRow.appendChild(minuteSelect);
|
|
159
|
+
element.appendChild(timeRow);
|
|
160
|
+
|
|
161
|
+
const footer = document.createElement('div');
|
|
162
|
+
footer.className = 'temporal-picker-footer';
|
|
163
|
+
|
|
164
|
+
const clearButton = document.createElement('button');
|
|
165
|
+
clearButton.type = 'button';
|
|
166
|
+
clearButton.className = 'temporal-picker-button secondary';
|
|
167
|
+
clearButton.textContent = 'Clear';
|
|
168
|
+
clearButton.addEventListener('click', () => {
|
|
169
|
+
onClear?.();
|
|
170
|
+
close();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const todayButton = document.createElement('button');
|
|
174
|
+
todayButton.type = 'button';
|
|
175
|
+
todayButton.className = 'temporal-picker-button secondary';
|
|
176
|
+
todayButton.textContent = 'Today';
|
|
177
|
+
todayButton.addEventListener('click', () => {
|
|
178
|
+
state.selectedDate = new Date();
|
|
179
|
+
state.visibleMonth = new Date(state.selectedDate.getFullYear(), state.selectedDate.getMonth(), 1);
|
|
180
|
+
render(state);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const applyButton = document.createElement('button');
|
|
184
|
+
applyButton.type = 'button';
|
|
185
|
+
applyButton.className = 'temporal-picker-button primary';
|
|
186
|
+
applyButton.textContent = 'Apply';
|
|
187
|
+
applyButton.addEventListener('click', () => {
|
|
188
|
+
onApply?.(formatInputDate(state.selectedDate));
|
|
189
|
+
close();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
footer.appendChild(clearButton);
|
|
193
|
+
footer.appendChild(todayButton);
|
|
194
|
+
footer.appendChild(applyButton);
|
|
195
|
+
element.appendChild(footer);
|
|
196
|
+
|
|
197
|
+
position(input, element);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const open = (input, options = {}) => {
|
|
201
|
+
if (activePicker?.input === input) return;
|
|
202
|
+
close();
|
|
203
|
+
|
|
204
|
+
const picker = document.createElement('div');
|
|
205
|
+
picker.className = 'temporal-picker-popover';
|
|
206
|
+
picker.setAttribute('role', 'dialog');
|
|
207
|
+
picker.setAttribute('aria-label', `Select ${input.dataset.parameterName || 'date and time'}`);
|
|
208
|
+
document.body.appendChild(picker);
|
|
209
|
+
|
|
210
|
+
const selectedDate = parseInputDate(input.value);
|
|
211
|
+
const state = {
|
|
212
|
+
element: picker,
|
|
213
|
+
input,
|
|
214
|
+
selectedDate,
|
|
215
|
+
visibleMonth: new Date(selectedDate.getFullYear(), selectedDate.getMonth(), 1),
|
|
216
|
+
onApply: options.onApply || (value => { input.value = value; }),
|
|
217
|
+
onClear: options.onClear || (() => { input.value = ''; })
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const outsideHandler = (event) => {
|
|
221
|
+
if (picker.contains(event.target) || event.target === input) return;
|
|
222
|
+
close();
|
|
223
|
+
};
|
|
224
|
+
const keyHandler = (event) => event.key === 'Escape' && close();
|
|
225
|
+
const repositionHandler = () => position(input, picker);
|
|
226
|
+
|
|
227
|
+
activePicker = {
|
|
228
|
+
element: picker,
|
|
229
|
+
input,
|
|
230
|
+
cleanup: () => {
|
|
231
|
+
document.removeEventListener('mousedown', outsideHandler);
|
|
232
|
+
document.removeEventListener('keydown', keyHandler);
|
|
233
|
+
window.removeEventListener('resize', repositionHandler);
|
|
234
|
+
window.removeEventListener('scroll', repositionHandler, true);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
document.addEventListener('mousedown', outsideHandler);
|
|
239
|
+
document.addEventListener('keydown', keyHandler);
|
|
240
|
+
window.addEventListener('resize', repositionHandler);
|
|
241
|
+
window.addEventListener('scroll', repositionHandler, true);
|
|
242
|
+
|
|
243
|
+
render(state);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
window.TemporalPicker = {open, close};
|
|
247
|
+
})();
|
|
@@ -7,6 +7,7 @@ html
|
|
|
7
7
|
- base_path = request.script_name
|
|
8
8
|
link[rel="icon" type="image/png" href="#{base_path}/icons/favicon.png"]
|
|
9
9
|
link[href="#{base_path}/vendor/maplibre-gl/maplibre-gl.css" rel='stylesheet']
|
|
10
|
+
link[href="#{base_path}/css/temporal_picker.css" rel='stylesheet']
|
|
10
11
|
script[src="#{base_path}/vendor/maplibre-gl/maplibre-gl.js"]
|
|
11
12
|
script[src="#{base_path}/vendor/maplibre-contour/index.min.js"]
|
|
12
13
|
script[src="#{base_path}/vendor/d3/d3.v7.min.js"]
|
|
@@ -14,6 +15,7 @@ html
|
|
|
14
15
|
script[src="#{base_path}/js/filters.js"]
|
|
15
16
|
script[src="#{base_path}/js/contour.js"]
|
|
16
17
|
script[src="#{base_path}/js/tilegrid.js"]
|
|
18
|
+
script[src="#{base_path}/js/temporal_picker.js"]
|
|
17
19
|
style
|
|
18
20
|
| * { margin: 0; padding: 0; box-sizing: border-box; }
|
|
19
21
|
| html { height: 100%; }
|
|
@@ -387,17 +389,44 @@ html
|
|
|
387
389
|
| .style-parameter-row {
|
|
388
390
|
| display: flex; flex-direction: column; gap: 4px;
|
|
389
391
|
| }
|
|
392
|
+
| .style-parameter-header {
|
|
393
|
+
| display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
394
|
+
| }
|
|
390
395
|
| .style-parameter-label {
|
|
391
396
|
| color: #808080; font-size: 10px; font-weight: bold;
|
|
392
397
|
| text-transform: uppercase;
|
|
393
398
|
| }
|
|
399
|
+
| .style-parameter-counts {
|
|
400
|
+
| color: #6897bb; font-family: 'Courier New', monospace; font-size: 10px;
|
|
401
|
+
| white-space: nowrap;
|
|
402
|
+
| }
|
|
403
|
+
| .style-parameter-input-group {
|
|
404
|
+
| display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px;
|
|
405
|
+
| }
|
|
406
|
+
| .style-parameter-input-group.temporal {
|
|
407
|
+
| grid-template-columns: minmax(0, 1fr);
|
|
408
|
+
| }
|
|
394
409
|
| .style-parameter-input {
|
|
395
410
|
| width: 100%; background: #313335; color: #a9b7c6; border: 1px solid #555555;
|
|
396
411
|
| border-radius: 3px; padding: 6px 8px; font-size: 11px; box-sizing: border-box;
|
|
412
|
+
| color-scheme: dark;
|
|
397
413
|
| }
|
|
398
414
|
| .style-parameter-input:focus {
|
|
399
415
|
| outline: none; border-color: #6897bb; box-shadow: 0 0 0 1px #6897bb;
|
|
400
416
|
| }
|
|
417
|
+
| .style-parameter-input.temporal {
|
|
418
|
+
| font-family: 'Courier New', monospace; font-size: 11px; cursor: pointer;
|
|
419
|
+
| }
|
|
420
|
+
| .style-parameter-context {
|
|
421
|
+
| display: flex; flex-direction: column; gap: 2px;
|
|
422
|
+
| color: #999999; font-size: 10px; line-height: 1.25;
|
|
423
|
+
| }
|
|
424
|
+
| .style-parameter-context-row {
|
|
425
|
+
| display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
426
|
+
| }
|
|
427
|
+
| .style-parameter-context-key {
|
|
428
|
+
| color: #808080; text-transform: uppercase; margin-right: 4px;
|
|
429
|
+
| }
|
|
401
430
|
| .style-parameter-actions {
|
|
402
431
|
| display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
|
|
403
432
|
| }
|
|
@@ -405,13 +434,18 @@ html
|
|
|
405
434
|
| flex: 1; min-height: 0; max-height: calc(80vh - 245px); width: max-content; max-width: 100%;
|
|
406
435
|
| overflow: hidden;
|
|
407
436
|
| }
|
|
408
|
-
| .control-panel::-webkit-scrollbar
|
|
409
|
-
| .
|
|
410
|
-
| .control-panel::-webkit-scrollbar-
|
|
437
|
+
| .control-panel::-webkit-scrollbar,
|
|
438
|
+
| .style-parameters-body::-webkit-scrollbar { width: 8px; }
|
|
439
|
+
| .control-panel::-webkit-scrollbar-track,
|
|
440
|
+
| .style-parameters-body::-webkit-scrollbar-track { background: #3c3f41; border-radius: 4px; }
|
|
441
|
+
| .control-panel::-webkit-scrollbar-thumb,
|
|
442
|
+
| .style-parameters-body::-webkit-scrollbar-thumb {
|
|
411
443
|
| background: #555555; border-radius: 4px; border: 1px solid #3c3f41;
|
|
412
444
|
| }
|
|
413
|
-
| .control-panel::-webkit-scrollbar-thumb:hover
|
|
414
|
-
| .
|
|
445
|
+
| .control-panel::-webkit-scrollbar-thumb:hover,
|
|
446
|
+
| .style-parameters-body::-webkit-scrollbar-thumb:hover { background: #666666; }
|
|
447
|
+
| .control-panel,
|
|
448
|
+
| .style-parameters-body { scrollbar-width: thin; scrollbar-color: #555555 #3c3f41; }
|
|
415
449
|
| .control-panel.active { display: flex; }
|
|
416
450
|
| .control-panel-header {
|
|
417
451
|
| flex-shrink: 0; border-bottom: 1px solid #555555; padding-bottom: 8px; margin-bottom: 8px;
|
|
@@ -156,7 +156,7 @@ javascript:
|
|
|
156
156
|
let originalStyle = null;
|
|
157
157
|
let styleParameterDefinitions = new Map();
|
|
158
158
|
let styleParameterValues = {};
|
|
159
|
-
let
|
|
159
|
+
let parameterizedUrlRules = [];
|
|
160
160
|
|
|
161
161
|
const toDomId = (prefix, id) => `${prefix}-${String(id).replace(/[^a-zA-Z0-9_-]/g, '_')}`;
|
|
162
162
|
const noCacheRequestOptions = () => mapCacheDisabled ? {cache: 'no-store'} : undefined;
|
|
@@ -169,7 +169,8 @@ javascript:
|
|
|
169
169
|
return [];
|
|
170
170
|
};
|
|
171
171
|
|
|
172
|
-
const
|
|
172
|
+
const isTemporalParameter = (name) => /(^|_|-)(time|date|datetime)($|_|-)/i.test(name);
|
|
173
|
+
const inferParameterInputType = (name) => isTemporalParameter(name) ? 'datetime-local' : 'text';
|
|
173
174
|
|
|
174
175
|
const parameterInputToQueryValue = (name, value) => {
|
|
175
176
|
if (!value) return '';
|
|
@@ -211,9 +212,24 @@ javascript:
|
|
|
211
212
|
return prefix ? new URL(prefix, window.location.href).toString() : null;
|
|
212
213
|
};
|
|
213
214
|
|
|
214
|
-
const rememberParameterizedUrl = (template) => {
|
|
215
|
+
const rememberParameterizedUrl = (template, parameterNames) => {
|
|
216
|
+
const names = normalizeQueryParams(parameterNames);
|
|
217
|
+
if (!names.length) return;
|
|
218
|
+
|
|
215
219
|
const prefix = urlPrefixFromTemplate(template);
|
|
216
|
-
|
|
220
|
+
if (!prefix) return;
|
|
221
|
+
|
|
222
|
+
const existing = parameterizedUrlRules.find(rule => rule.prefix === prefix);
|
|
223
|
+
if (existing) {
|
|
224
|
+
names.forEach(name => existing.parameterNames.add(name));
|
|
225
|
+
} else {
|
|
226
|
+
parameterizedUrlRules.push({prefix, parameterNames: new Set(names)});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const rememberParameterizedSourceUrls = (sourceDef, parameterNames) => {
|
|
231
|
+
[sourceDef?.url, sourceDef?.meta_url, sourceDef?.data].forEach(url => rememberParameterizedUrl(url, parameterNames));
|
|
232
|
+
(sourceDef?.tiles || []).forEach(tileUrl => rememberParameterizedUrl(tileUrl, parameterNames));
|
|
217
233
|
};
|
|
218
234
|
|
|
219
235
|
const appendStyleParametersToUrl = (resourceUrl, values = styleParameterValues, parameterNames = Object.keys(values)) => {
|
|
@@ -242,16 +258,13 @@ javascript:
|
|
|
242
258
|
const params = sourceDeclaredParameters(sourceDef);
|
|
243
259
|
params.forEach(name => mergeStyleParameterDefinition(name, sourceName));
|
|
244
260
|
|
|
245
|
-
|
|
246
|
-
[sourceDef.url, sourceDef.meta_url, sourceDef.data].forEach(rememberParameterizedUrl);
|
|
247
|
-
(sourceDef.tiles || []).forEach(rememberParameterizedUrl);
|
|
248
|
-
}
|
|
261
|
+
rememberParameterizedSourceUrls(sourceDef, params);
|
|
249
262
|
});
|
|
250
263
|
};
|
|
251
264
|
|
|
252
265
|
const fetchSourceMetadata = async (url) => {
|
|
253
266
|
try {
|
|
254
|
-
const response = await fetch(
|
|
267
|
+
const response = await fetch(url, noCacheRequestOptions());
|
|
255
268
|
return response.ok ? response.json() : null;
|
|
256
269
|
} catch (e) {
|
|
257
270
|
console.warn('Could not inspect source metadata:', url, e);
|
|
@@ -272,7 +285,9 @@ javascript:
|
|
|
272
285
|
|
|
273
286
|
const params = normalizeQueryParams(metadata.query_params || metadata.queryParams);
|
|
274
287
|
params.forEach(name => mergeStyleParameterDefinition(name, sourceName));
|
|
275
|
-
|
|
288
|
+
const parameterNames = getSourceParameterNames(sourceName, sourceDef);
|
|
289
|
+
rememberParameterizedSourceUrls(sourceDef, parameterNames);
|
|
290
|
+
(metadata.tiles || []).forEach(tileUrl => rememberParameterizedUrl(tileUrl, parameterNames));
|
|
276
291
|
}
|
|
277
292
|
}));
|
|
278
293
|
};
|
|
@@ -297,6 +312,53 @@ javascript:
|
|
|
297
312
|
return values;
|
|
298
313
|
};
|
|
299
314
|
|
|
315
|
+
const normalizeLocalizedLabel = (value) => {
|
|
316
|
+
if (!value) return null;
|
|
317
|
+
if (typeof value === 'string') return value;
|
|
318
|
+
if (typeof value === 'object') return value.title || value.name || value.label || null;
|
|
319
|
+
return String(value);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const getLocalizedMetadataLabel = (id, style = originalStyle) => {
|
|
323
|
+
const locale = style?.metadata?.locale;
|
|
324
|
+
if (!locale || !id) return id;
|
|
325
|
+
|
|
326
|
+
for (const lang of ['en-US', 'en', 'ru']) {
|
|
327
|
+
const label = normalizeLocalizedLabel(locale[lang]?.[id]);
|
|
328
|
+
if (label) return label;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
for (const lang in locale) {
|
|
332
|
+
const label = normalizeLocalizedLabel(locale[lang]?.[id]);
|
|
333
|
+
if (label) return label;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return id;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const compactList = (items, limit = 3) => {
|
|
340
|
+
const unique = [...new Set(items.filter(Boolean))];
|
|
341
|
+
return unique.length > limit
|
|
342
|
+
? `${unique.slice(0, limit).join(', ')} +${unique.length - limit}`
|
|
343
|
+
: unique.join(', ');
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const getStyleParameterContext = (definition) => {
|
|
347
|
+
const style = originalStyle || currentStyle;
|
|
348
|
+
const sources = [...definition.sources].sort();
|
|
349
|
+
const layers = (style?.layers || []).filter(layer => sources.includes(layer.source));
|
|
350
|
+
const filterIds = [...new Set(layers.map(layer => layer.metadata?.filter_id).filter(Boolean))].sort();
|
|
351
|
+
const filterLabels = filterIds.map(id => getLocalizedMetadataLabel(id, style));
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
sources,
|
|
355
|
+
layers: layers.map(layer => layer.id).sort(),
|
|
356
|
+
filterLabels: filterLabels.length ? filterLabels : sources,
|
|
357
|
+
sourceSummary: compactList(sources),
|
|
358
|
+
filterSummary: compactList(filterLabels.length ? filterLabels : sources)
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
|
|
300
362
|
const renderStyleParameterControls = () => {
|
|
301
363
|
const panel = document.getElementById('style-parameters-panel');
|
|
302
364
|
const fields = document.getElementById('style-parameter-fields');
|
|
@@ -309,24 +371,79 @@ javascript:
|
|
|
309
371
|
fields.innerHTML = '';
|
|
310
372
|
|
|
311
373
|
definitions.forEach(definition => {
|
|
312
|
-
const row = document.createElement('
|
|
374
|
+
const row = document.createElement('div');
|
|
313
375
|
row.className = 'style-parameter-row';
|
|
314
|
-
|
|
376
|
+
const context = getStyleParameterContext(definition);
|
|
315
377
|
|
|
316
|
-
const
|
|
378
|
+
const header = document.createElement('div');
|
|
379
|
+
header.className = 'style-parameter-header';
|
|
380
|
+
const label = document.createElement('label');
|
|
317
381
|
label.className = 'style-parameter-label';
|
|
382
|
+
label.htmlFor = `style-param-${definition.name}`;
|
|
318
383
|
label.textContent = definition.name;
|
|
384
|
+
const counts = document.createElement('span');
|
|
385
|
+
counts.className = 'style-parameter-counts';
|
|
386
|
+
counts.textContent = `${context.sources.length} src / ${context.layers.length} lyr`;
|
|
387
|
+
counts.title = `${context.sources.length} sources, ${context.layers.length} layers`;
|
|
388
|
+
header.appendChild(label);
|
|
389
|
+
header.appendChild(counts);
|
|
319
390
|
|
|
320
391
|
const input = document.createElement('input');
|
|
321
392
|
input.className = 'style-parameter-input';
|
|
322
393
|
input.id = `style-param-${definition.name}`;
|
|
323
|
-
input.type = inferParameterInputType(definition.name);
|
|
394
|
+
input.type = isTemporalParameter(definition.name) ? 'text' : inferParameterInputType(definition.name);
|
|
324
395
|
input.value = queryValueToParameterInput(definition.name, styleParameterValues[definition.name]);
|
|
325
396
|
input.dataset.parameterName = definition.name;
|
|
326
|
-
input.title = [
|
|
397
|
+
input.title = [
|
|
398
|
+
`Sources: ${context.sources.join(', ')}`,
|
|
399
|
+
`Layers: ${context.layers.join(', ')}`
|
|
400
|
+
].join('\n');
|
|
401
|
+
if (isTemporalParameter(definition.name)) {
|
|
402
|
+
input.classList.add('temporal');
|
|
403
|
+
input.readOnly = true;
|
|
404
|
+
input.placeholder = 'Select date and time';
|
|
405
|
+
input.setAttribute('aria-haspopup', 'dialog');
|
|
406
|
+
input.addEventListener('click', () => window.TemporalPicker.open(input));
|
|
407
|
+
input.addEventListener('focus', () => window.TemporalPicker.open(input));
|
|
408
|
+
input.addEventListener('keydown', event => {
|
|
409
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
410
|
+
event.preventDefault();
|
|
411
|
+
window.TemporalPicker.open(input);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
327
415
|
|
|
328
|
-
|
|
329
|
-
|
|
416
|
+
const parameterControl = document.createElement('div');
|
|
417
|
+
parameterControl.className = isTemporalParameter(definition.name)
|
|
418
|
+
? 'style-parameter-input-group temporal'
|
|
419
|
+
: 'style-parameter-input-group';
|
|
420
|
+
parameterControl.appendChild(input);
|
|
421
|
+
|
|
422
|
+
const contextBlock = document.createElement('div');
|
|
423
|
+
contextBlock.className = 'style-parameter-context';
|
|
424
|
+
contextBlock.title = input.title;
|
|
425
|
+
|
|
426
|
+
const usedBy = document.createElement('span');
|
|
427
|
+
usedBy.className = 'style-parameter-context-row';
|
|
428
|
+
const usedByKey = document.createElement('span');
|
|
429
|
+
usedByKey.className = 'style-parameter-context-key';
|
|
430
|
+
usedByKey.textContent = 'Used by';
|
|
431
|
+
usedBy.appendChild(usedByKey);
|
|
432
|
+
usedBy.appendChild(document.createTextNode(` ${context.filterSummary || 'No linked layers'}`));
|
|
433
|
+
contextBlock.appendChild(usedBy);
|
|
434
|
+
|
|
435
|
+
const sourceInfo = document.createElement('span');
|
|
436
|
+
sourceInfo.className = 'style-parameter-context-row';
|
|
437
|
+
const sourceKey = document.createElement('span');
|
|
438
|
+
sourceKey.className = 'style-parameter-context-key';
|
|
439
|
+
sourceKey.textContent = 'Sources';
|
|
440
|
+
sourceInfo.appendChild(sourceKey);
|
|
441
|
+
sourceInfo.appendChild(document.createTextNode(` ${context.sourceSummary || 'None'}`));
|
|
442
|
+
contextBlock.appendChild(sourceInfo);
|
|
443
|
+
|
|
444
|
+
row.appendChild(header);
|
|
445
|
+
row.appendChild(parameterControl);
|
|
446
|
+
row.appendChild(contextBlock);
|
|
330
447
|
fields.appendChild(row);
|
|
331
448
|
});
|
|
332
449
|
|
|
@@ -348,7 +465,7 @@ javascript:
|
|
|
348
465
|
const initializeStyleParameters = async (style) => {
|
|
349
466
|
styleParameterDefinitions = new Map();
|
|
350
467
|
styleParameterValues = {};
|
|
351
|
-
|
|
468
|
+
parameterizedUrlRules = [];
|
|
352
469
|
|
|
353
470
|
collectStyleSourceParameters(style);
|
|
354
471
|
styleParameterDefinitions.forEach((definition, name) => {
|
|
@@ -389,13 +506,25 @@ javascript:
|
|
|
389
506
|
return modifiedStyle;
|
|
390
507
|
};
|
|
391
508
|
|
|
392
|
-
const
|
|
393
|
-
if (!Object.values(styleParameterValues).some(value => value !== undefined && value !== null && value !== '')) return false;
|
|
509
|
+
const parameterizedUrlRuleFor = (resourceUrl) => {
|
|
394
510
|
const absolute = new URL(resourceUrl, window.location.href).toString();
|
|
395
|
-
return
|
|
511
|
+
return parameterizedUrlRules
|
|
512
|
+
.filter(rule => absolute.startsWith(rule.prefix))
|
|
513
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)[0] || null;
|
|
396
514
|
};
|
|
397
515
|
|
|
398
|
-
const patchRequestUrl = (resourceUrl) =>
|
|
516
|
+
const patchRequestUrl = (resourceUrl) => {
|
|
517
|
+
const rule = parameterizedUrlRuleFor(resourceUrl);
|
|
518
|
+
if (!rule) return resourceUrl;
|
|
519
|
+
|
|
520
|
+
const parameterNames = [...rule.parameterNames];
|
|
521
|
+
if (!parameterNames.some(name => {
|
|
522
|
+
const value = styleParameterValues[name];
|
|
523
|
+
return value !== undefined && value !== null && value !== '';
|
|
524
|
+
})) return resourceUrl;
|
|
525
|
+
|
|
526
|
+
return appendStyleParametersToUrl(resourceUrl, styleParameterValues, parameterNames);
|
|
527
|
+
};
|
|
399
528
|
|
|
400
529
|
const BOTTOM_OVERLAY_IDS = ['loading-indicator'];
|
|
401
530
|
const BOTTOM_OVERLAY_BASE_OFFSET = 20;
|
|
@@ -50,9 +50,23 @@ RSpec.describe MapLibrePreview do
|
|
|
50
50
|
expect(last_response.body).to include('basemapOpacity')
|
|
51
51
|
expect(last_response.body).to include('terrainExaggeration')
|
|
52
52
|
expect(last_response.body).to include('styleParameterDefinitions')
|
|
53
|
+
expect(last_response.body).to include('parameterizedUrlRules')
|
|
53
54
|
expect(last_response.body).to include('sourceDeclaredParameters')
|
|
54
55
|
expect(last_response.body).to include('collectSourceMetadataParameters')
|
|
55
56
|
expect(last_response.body).to include('applyStyleParametersToStyle')
|
|
57
|
+
expect(last_response.body).to include('rememberParameterizedSourceUrls')
|
|
58
|
+
expect(last_response.body).to include('parameterizedUrlRuleFor')
|
|
59
|
+
expect(last_response.body).to include('getStyleParameterContext')
|
|
60
|
+
expect(last_response.body).to include('isTemporalParameter')
|
|
61
|
+
expect(last_response.body).to include('/css/temporal_picker.css')
|
|
62
|
+
expect(last_response.body).to include('/js/temporal_picker.js')
|
|
63
|
+
expect(last_response.body).to include('window.TemporalPicker.open')
|
|
64
|
+
expect(last_response.body).to include('style-parameter-input-group')
|
|
65
|
+
expect(last_response.body).to include('style-parameter-counts')
|
|
66
|
+
expect(last_response.body).to include('style-parameter-context')
|
|
67
|
+
expect(last_response.body).to include('Used by')
|
|
68
|
+
expect(last_response.body).to include('Sources:')
|
|
69
|
+
expect(last_response.body).not_to include("absolute.includes('/rb_tiles/')")
|
|
56
70
|
expect(last_response.body).to include('layoutBottomOverlays')
|
|
57
71
|
expect(last_response.body).to include('showCollisionBoxes')
|
|
58
72
|
expect(last_response.body).to include('showOverdrawInspector')
|
|
@@ -68,7 +82,7 @@ RSpec.describe MapLibrePreview do
|
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
it 'serves all required JavaScript modules' do
|
|
71
|
-
%w[/js/overlay_layout.js /js/filters.js /js/contour.js /js/tilegrid.js /vendor/maplibre-gl/maplibre-gl.js /vendor/maplibre-contour/index.min.js /vendor/d3/d3.v7.min.js].each do |js_file|
|
|
85
|
+
%w[/js/overlay_layout.js /js/filters.js /js/contour.js /js/tilegrid.js /js/temporal_picker.js /vendor/maplibre-gl/maplibre-gl.js /vendor/maplibre-contour/index.min.js /vendor/d3/d3.v7.min.js].each do |js_file|
|
|
72
86
|
get js_file
|
|
73
87
|
expect(last_response).to be_ok
|
|
74
88
|
expect(last_response.content_type).to include('javascript')
|
|
@@ -77,11 +91,13 @@ RSpec.describe MapLibrePreview do
|
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
it 'serves required stylesheets' do
|
|
80
|
-
|
|
94
|
+
%w[/vendor/maplibre-gl/maplibre-gl.css /css/temporal_picker.css].each do |css_file|
|
|
95
|
+
get css_file
|
|
81
96
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
expect(last_response).to be_ok
|
|
98
|
+
expect(last_response.content_type).to include('text/css')
|
|
99
|
+
expect(last_response.body).not_to be_empty
|
|
100
|
+
end
|
|
85
101
|
end
|
|
86
102
|
end
|
|
87
103
|
|
|
@@ -93,10 +109,12 @@ RSpec.describe MapLibrePreview do
|
|
|
93
109
|
body = last_response.body
|
|
94
110
|
|
|
95
111
|
expect(body).to include('/vendor/maplibre-gl/maplibre-gl.css')
|
|
112
|
+
expect(body).to include('/css/temporal_picker.css')
|
|
96
113
|
expect(body).to include('/vendor/maplibre-gl/maplibre-gl.js')
|
|
97
114
|
expect(body).to include('/vendor/maplibre-contour/index.min.js')
|
|
98
115
|
expect(body).to include('/vendor/d3/d3.v7.min.js')
|
|
99
116
|
expect(body).to include('/js/overlay_layout.js')
|
|
117
|
+
expect(body).to include('/js/temporal_picker.js')
|
|
100
118
|
expect(body).not_to include('unpkg.com')
|
|
101
119
|
expect(body).not_to include('d3js.org')
|
|
102
120
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maplibre-preview
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Ludov
|
|
@@ -201,9 +201,11 @@ files:
|
|
|
201
201
|
- bin/maplibre-preview
|
|
202
202
|
- docs/README_RU.md
|
|
203
203
|
- lib/maplibre-preview.rb
|
|
204
|
+
- lib/maplibre-preview/public/css/temporal_picker.css
|
|
204
205
|
- lib/maplibre-preview/public/js/contour.js
|
|
205
206
|
- lib/maplibre-preview/public/js/filters.js
|
|
206
207
|
- lib/maplibre-preview/public/js/overlay_layout.js
|
|
208
|
+
- lib/maplibre-preview/public/js/temporal_picker.js
|
|
207
209
|
- lib/maplibre-preview/public/js/tilegrid.js
|
|
208
210
|
- lib/maplibre-preview/public/vendor/d3/d3.v7.min.js
|
|
209
211
|
- lib/maplibre-preview/public/vendor/maplibre-contour/index.min.js
|