@mapbox/mcp-server 0.17.0-dev.1 → 0.17.0-dev.2
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.
- package/dist/commonjs/resources/BaseResource.d.ts +7 -0
- package/dist/commonjs/resources/BaseResource.d.ts.map +1 -1
- package/dist/commonjs/resources/BaseResource.js +13 -2
- package/dist/commonjs/resources/BaseResource.js.map +1 -1
- package/dist/commonjs/resources/category-list/CategoryListResource.d.ts +7 -2
- package/dist/commonjs/resources/category-list/CategoryListResource.d.ts.map +1 -1
- package/dist/commonjs/resources/category-list/CategoryListResource.js +14 -4
- package/dist/commonjs/resources/category-list/CategoryListResource.js.map +1 -1
- package/dist/commonjs/resources/place-photo/PlacePhotoResource.d.ts +1 -1
- package/dist/commonjs/resources/place-photo/PlacePhotoResource.js +2 -2
- package/dist/commonjs/resources/place-photo/PlacePhotoResource.js.map +1 -1
- package/dist/commonjs/resources/resourceRegistry.d.ts +2 -1
- package/dist/commonjs/resources/resourceRegistry.d.ts.map +1 -1
- package/dist/commonjs/resources/resourceRegistry.js +10 -5
- package/dist/commonjs/resources/resourceRegistry.js.map +1 -1
- package/dist/commonjs/resources/tool-response/ToolResponseResource.d.ts +6 -5
- package/dist/commonjs/resources/tool-response/ToolResponseResource.d.ts.map +1 -1
- package/dist/commonjs/resources/tool-response/ToolResponseResource.js +29 -5
- package/dist/commonjs/resources/tool-response/ToolResponseResource.js.map +1 -1
- package/dist/commonjs/resources/ui-apps/StaticMapViewerUIResource.d.ts +28 -0
- package/dist/commonjs/resources/ui-apps/StaticMapViewerUIResource.d.ts.map +1 -0
- package/dist/commonjs/resources/ui-apps/StaticMapViewerUIResource.js +64 -0
- package/dist/commonjs/resources/ui-apps/StaticMapViewerUIResource.js.map +1 -0
- package/dist/commonjs/resources/ui-apps/mapAppHtml.d.ts.map +1 -1
- package/dist/commonjs/resources/ui-apps/mapAppHtml.js +936 -223
- package/dist/commonjs/resources/ui-apps/mapAppHtml.js.map +1 -1
- package/dist/commonjs/resources/ui-apps/staticMapViewerHtml.d.ts +17 -0
- package/dist/commonjs/resources/ui-apps/staticMapViewerHtml.d.ts.map +1 -0
- package/dist/commonjs/resources/ui-apps/staticMapViewerHtml.js +180 -0
- package/dist/commonjs/resources/ui-apps/staticMapViewerHtml.js.map +1 -0
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.d.ts.map +1 -1
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.input.schema.d.ts +1 -0
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.input.schema.d.ts.map +1 -1
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.input.schema.js +10 -6
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.input.schema.js.map +1 -1
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.js +26 -12
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.js.map +1 -1
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.output.schema.d.ts +1 -0
- package/dist/commonjs/tools/render-map-tool/RenderMapTool.output.schema.d.ts.map +1 -1
- package/dist/commonjs/tools/resource-reader-tool/ResourceReaderTool.d.ts.map +1 -1
- package/dist/commonjs/tools/resource-reader-tool/ResourceReaderTool.js +43 -9
- package/dist/commonjs/tools/resource-reader-tool/ResourceReaderTool.js.map +1 -1
- package/dist/commonjs/tools/static-map-image-tool/StaticMapImageTool.d.ts +8 -1
- package/dist/commonjs/tools/static-map-image-tool/StaticMapImageTool.d.ts.map +1 -1
- package/dist/commonjs/tools/static-map-image-tool/StaticMapImageTool.js +113 -90
- package/dist/commonjs/tools/static-map-image-tool/StaticMapImageTool.js.map +1 -1
- package/dist/commonjs/tools/static-map-image-tool/buildStaticMapRequestUrl.d.ts +23 -0
- package/dist/commonjs/tools/static-map-image-tool/buildStaticMapRequestUrl.d.ts.map +1 -0
- package/dist/commonjs/tools/static-map-image-tool/buildStaticMapRequestUrl.js +84 -0
- package/dist/commonjs/tools/static-map-image-tool/buildStaticMapRequestUrl.js.map +1 -0
- package/dist/commonjs/utils/mapAppPayload.d.ts +15 -6
- package/dist/commonjs/utils/mapAppPayload.d.ts.map +1 -1
- package/dist/commonjs/utils/mapAppPayload.js +2 -1
- package/dist/commonjs/utils/mapAppPayload.js.map +1 -1
- package/dist/commonjs/utils/mapboxPublicToken.d.ts +5 -1
- package/dist/commonjs/utils/mapboxPublicToken.d.ts.map +1 -1
- package/dist/commonjs/utils/mapboxPublicToken.js +6 -1
- package/dist/commonjs/utils/mapboxPublicToken.js.map +1 -1
- package/dist/commonjs/utils/responseRef.d.ts +1 -1
- package/dist/commonjs/utils/responseRef.d.ts.map +1 -1
- package/dist/commonjs/utils/responseRef.js +1 -1
- package/dist/commonjs/utils/responseRef.js.map +1 -1
- package/dist/commonjs/version.json +3 -3
- package/dist/esm/resources/BaseResource.d.ts +7 -0
- package/dist/esm/resources/BaseResource.d.ts.map +1 -1
- package/dist/esm/resources/BaseResource.js +13 -2
- package/dist/esm/resources/BaseResource.js.map +1 -1
- package/dist/esm/resources/category-list/CategoryListResource.d.ts +7 -2
- package/dist/esm/resources/category-list/CategoryListResource.d.ts.map +1 -1
- package/dist/esm/resources/category-list/CategoryListResource.js +14 -4
- package/dist/esm/resources/category-list/CategoryListResource.js.map +1 -1
- package/dist/esm/resources/place-photo/PlacePhotoResource.d.ts +1 -1
- package/dist/esm/resources/place-photo/PlacePhotoResource.js +2 -2
- package/dist/esm/resources/place-photo/PlacePhotoResource.js.map +1 -1
- package/dist/esm/resources/resourceRegistry.d.ts +2 -1
- package/dist/esm/resources/resourceRegistry.d.ts.map +1 -1
- package/dist/esm/resources/resourceRegistry.js +10 -5
- package/dist/esm/resources/resourceRegistry.js.map +1 -1
- package/dist/esm/resources/tool-response/ToolResponseResource.d.ts +6 -5
- package/dist/esm/resources/tool-response/ToolResponseResource.d.ts.map +1 -1
- package/dist/esm/resources/tool-response/ToolResponseResource.js +29 -5
- package/dist/esm/resources/tool-response/ToolResponseResource.js.map +1 -1
- package/dist/esm/resources/ui-apps/StaticMapViewerUIResource.d.ts +28 -0
- package/dist/esm/resources/ui-apps/StaticMapViewerUIResource.d.ts.map +1 -0
- package/dist/esm/resources/ui-apps/StaticMapViewerUIResource.js +59 -0
- package/dist/esm/resources/ui-apps/StaticMapViewerUIResource.js.map +1 -0
- package/dist/esm/resources/ui-apps/mapAppHtml.d.ts.map +1 -1
- package/dist/esm/resources/ui-apps/mapAppHtml.js +936 -223
- package/dist/esm/resources/ui-apps/mapAppHtml.js.map +1 -1
- package/dist/esm/resources/ui-apps/staticMapViewerHtml.d.ts +17 -0
- package/dist/esm/resources/ui-apps/staticMapViewerHtml.d.ts.map +1 -0
- package/dist/esm/resources/ui-apps/staticMapViewerHtml.js +177 -0
- package/dist/esm/resources/ui-apps/staticMapViewerHtml.js.map +1 -0
- package/dist/esm/tools/render-map-tool/RenderMapTool.d.ts.map +1 -1
- package/dist/esm/tools/render-map-tool/RenderMapTool.input.schema.d.ts +1 -0
- package/dist/esm/tools/render-map-tool/RenderMapTool.input.schema.d.ts.map +1 -1
- package/dist/esm/tools/render-map-tool/RenderMapTool.input.schema.js +10 -6
- package/dist/esm/tools/render-map-tool/RenderMapTool.input.schema.js.map +1 -1
- package/dist/esm/tools/render-map-tool/RenderMapTool.js +26 -12
- package/dist/esm/tools/render-map-tool/RenderMapTool.js.map +1 -1
- package/dist/esm/tools/render-map-tool/RenderMapTool.output.schema.d.ts +1 -0
- package/dist/esm/tools/render-map-tool/RenderMapTool.output.schema.d.ts.map +1 -1
- package/dist/esm/tools/resource-reader-tool/ResourceReaderTool.d.ts.map +1 -1
- package/dist/esm/tools/resource-reader-tool/ResourceReaderTool.js +43 -9
- package/dist/esm/tools/resource-reader-tool/ResourceReaderTool.js.map +1 -1
- package/dist/esm/tools/static-map-image-tool/StaticMapImageTool.d.ts +8 -1
- package/dist/esm/tools/static-map-image-tool/StaticMapImageTool.d.ts.map +1 -1
- package/dist/esm/tools/static-map-image-tool/StaticMapImageTool.js +114 -91
- package/dist/esm/tools/static-map-image-tool/StaticMapImageTool.js.map +1 -1
- package/dist/esm/tools/static-map-image-tool/buildStaticMapRequestUrl.d.ts +23 -0
- package/dist/esm/tools/static-map-image-tool/buildStaticMapRequestUrl.d.ts.map +1 -0
- package/dist/esm/tools/static-map-image-tool/buildStaticMapRequestUrl.js +78 -0
- package/dist/esm/tools/static-map-image-tool/buildStaticMapRequestUrl.js.map +1 -0
- package/dist/esm/utils/mapAppPayload.d.ts +15 -6
- package/dist/esm/utils/mapAppPayload.d.ts.map +1 -1
- package/dist/esm/utils/mapAppPayload.js +2 -1
- package/dist/esm/utils/mapAppPayload.js.map +1 -1
- package/dist/esm/utils/mapboxPublicToken.d.ts +5 -1
- package/dist/esm/utils/mapboxPublicToken.d.ts.map +1 -1
- package/dist/esm/utils/mapboxPublicToken.js +6 -1
- package/dist/esm/utils/mapboxPublicToken.js.map +1 -1
- package/dist/esm/utils/responseRef.d.ts +1 -1
- package/dist/esm/utils/responseRef.d.ts.map +1 -1
- package/dist/esm/utils/responseRef.js +1 -1
- package/dist/esm/utils/responseRef.js.map +1 -1
- package/dist/esm/version.json +3 -3
- package/package.json +1 -1
|
@@ -31,24 +31,62 @@ export function renderMapAppHtml(params) {
|
|
|
31
31
|
<link href="https://api.mapbox.com/mapbox-gl-js/v${glVersion}/mapbox-gl.css" rel="stylesheet">
|
|
32
32
|
<script src="https://api.mapbox.com/mapbox-gl-js/v${glVersion}/mapbox-gl.js"></script>
|
|
33
33
|
<style>
|
|
34
|
+
/* Light and dark, following the host's theme (see applyHostContext), or
|
|
35
|
+
the system's when the host sends none. Colors come from the host's own
|
|
36
|
+
style variables (MCP Apps hostContext.styles) when it sends them, so
|
|
37
|
+
the cards match the chat around them; the fallbacks are used
|
|
38
|
+
otherwise. The map, its pins and their name labels stay as they are. */
|
|
39
|
+
:root {
|
|
40
|
+
color-scheme: light;
|
|
41
|
+
--ui-surface: var(--color-background-primary, #ffffff);
|
|
42
|
+
--ui-surface-muted: var(--color-background-secondary, #f1f5f9);
|
|
43
|
+
--ui-text: var(--color-text-primary, #0f172a);
|
|
44
|
+
--ui-text-muted: var(--color-text-secondary, #475569);
|
|
45
|
+
--ui-icon-muted: #64748b;
|
|
46
|
+
--ui-shadow: 0 2px 10px rgba(15, 23, 42, 0.18);
|
|
47
|
+
--ui-error-bg: #ffebee; --ui-error-text: #b71c1c;
|
|
48
|
+
}
|
|
49
|
+
:root[data-theme="dark"] {
|
|
50
|
+
color-scheme: dark;
|
|
51
|
+
--ui-surface: var(--color-background-primary, #0f172a);
|
|
52
|
+
--ui-surface-muted: var(--color-background-secondary, #1e293b);
|
|
53
|
+
--ui-text: var(--color-text-primary, #f1f5f9);
|
|
54
|
+
--ui-text-muted: var(--color-text-secondary, #cbd5e1);
|
|
55
|
+
--ui-icon-muted: #94a3b8;
|
|
56
|
+
--ui-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
|
|
57
|
+
--ui-error-bg: #3b1214; --ui-error-text: #fca5a5;
|
|
58
|
+
}
|
|
34
59
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
35
60
|
html, body { width: 100%; height: 100%; overflow: hidden;
|
|
36
61
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
37
62
|
#map { position: absolute; inset: 0; }
|
|
38
63
|
#summary {
|
|
39
64
|
position: absolute; top: 12px; left: 12px; z-index: 10;
|
|
40
|
-
background:
|
|
65
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
41
66
|
padding: 8px 12px; border-radius: 6px;
|
|
42
67
|
font-size: 13px; font-weight: 500;
|
|
43
|
-
box-shadow:
|
|
68
|
+
box-shadow: var(--ui-shadow);
|
|
44
69
|
max-width: calc(100% - 24px); white-space: pre-line;
|
|
45
70
|
}
|
|
71
|
+
/* Bottom offsets keep the Mapbox logo (bottom left) and attribution
|
|
72
|
+
(bottom right) visible, as the Mapbox terms require. */
|
|
73
|
+
#legends {
|
|
74
|
+
position: absolute; bottom: 40px; left: 12px; z-index: 10;
|
|
75
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
|
|
76
|
+
}
|
|
46
77
|
#legend {
|
|
47
|
-
|
|
48
|
-
background: rgba(15, 23, 42, 0.88); color: #f1f5f9;
|
|
78
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
49
79
|
padding: 8px 12px; border-radius: 6px;
|
|
50
|
-
font-size: 11px;
|
|
80
|
+
font-size: 11px; box-shadow: var(--ui-shadow);
|
|
81
|
+
}
|
|
82
|
+
/* Traffic: Fast [4 colors] Slow, below any payload legend. */
|
|
83
|
+
#traffic-legend {
|
|
84
|
+
align-items: center; gap: 8px; padding: 8px; border-radius: 8px;
|
|
85
|
+
background: #fff; color: #05070a; font-size: 14px; line-height: 1.3;
|
|
86
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.12); white-space: nowrap;
|
|
51
87
|
}
|
|
88
|
+
#traffic-legend .bar { display: flex; gap: 2px; }
|
|
89
|
+
#traffic-legend .bar span { width: 20px; height: 10px; border-radius: 2px; }
|
|
52
90
|
#legend .row { display: flex; align-items: center; margin: 2px 0; }
|
|
53
91
|
#legend .swatch {
|
|
54
92
|
display: inline-block; width: 14px; height: 14px;
|
|
@@ -56,11 +94,11 @@ export function renderMapAppHtml(params) {
|
|
|
56
94
|
}
|
|
57
95
|
#loading {
|
|
58
96
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
59
|
-
color:
|
|
97
|
+
color: var(--ui-text-muted); font-size: 16px; z-index: 10; pointer-events: none;
|
|
60
98
|
}
|
|
61
99
|
#error {
|
|
62
100
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
63
|
-
color:
|
|
101
|
+
color: var(--ui-error-text); background: var(--ui-error-bg); border-radius: 8px;
|
|
64
102
|
padding: 20px; max-width: 400px; text-align: center; z-index: 10;
|
|
65
103
|
}
|
|
66
104
|
.marker-badge {
|
|
@@ -71,51 +109,99 @@ export function renderMapAppHtml(params) {
|
|
|
71
109
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
|
72
110
|
cursor: pointer;
|
|
73
111
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
background:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
/*
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
/* Selected place (see selectPlace): the pin inverts, like the card it
|
|
113
|
+
matches. */
|
|
114
|
+
.marker-badge.active {
|
|
115
|
+
background: #fff !important; color: #0f172a !important;
|
|
116
|
+
transform: scale(1.18); box-shadow: 0 3px 10px rgba(0,0,0,0.5);
|
|
117
|
+
}
|
|
118
|
+
/* Satellite / Traffic buttons under the zoom control (see buildLayerToggles). */
|
|
119
|
+
.mapboxgl-ctrl-group.layer-toggles button {
|
|
120
|
+
display: flex; align-items: center; justify-content: center; color: #333;
|
|
121
|
+
}
|
|
122
|
+
.mapboxgl-ctrl-group.layer-toggles button.active { color: #2563eb; background: #dbeafe; }
|
|
123
|
+
.mapboxgl-ctrl-group.layer-toggles button:focus:not(:focus-visible) { box-shadow: none; }
|
|
124
|
+
/* Every carousel place shows its name under the pin, with a white halo
|
|
125
|
+
instead of a box, the way map labels look. */
|
|
126
|
+
.marker-badge .marker-name {
|
|
127
|
+
position: absolute; top: 32px; left: 50%; transform: translateX(-50%);
|
|
128
|
+
white-space: nowrap; font-size: 11px; font-weight: 700; color: #0f172a;
|
|
129
|
+
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff,
|
|
130
|
+
0 0 3px #fff;
|
|
131
|
+
pointer-events: none;
|
|
132
|
+
}
|
|
133
|
+
/* A place pin (search results and other places in the carousel): a
|
|
134
|
+
white disc with the place's Maki icon, black when selected. Numbered
|
|
135
|
+
badges stay for markers whose number means something (visit order). */
|
|
136
|
+
.marker-badge.place {
|
|
137
|
+
width: 30px; height: 30px; background: #fff; color: #0f172a;
|
|
138
|
+
border: none; box-shadow: 0 1px 4px rgba(0,0,0,0.35);
|
|
139
|
+
}
|
|
140
|
+
.marker-badge.place svg { width: 16px; height: 16px; display: block; }
|
|
141
|
+
.marker-badge.place.active {
|
|
142
|
+
background: #0f172a !important; color: #fff !important;
|
|
143
|
+
}
|
|
144
|
+
/* Results carousel. Sits above the Mapbox logo and attribution, which
|
|
145
|
+
must stay visible; the legend moves up above it (see showCarousel). */
|
|
146
|
+
#carousel { position: absolute; left: 0; right: 0; bottom: 36px; z-index: 10; }
|
|
147
|
+
#carousel .card-list {
|
|
148
|
+
list-style: none; margin: 0; padding: 4px 32px 6px;
|
|
149
|
+
display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden;
|
|
150
|
+
scroll-snap-type: x mandatory; scrollbar-width: none;
|
|
151
|
+
}
|
|
152
|
+
#carousel .card-list::-webkit-scrollbar { display: none; }
|
|
153
|
+
#carousel .card {
|
|
154
|
+
flex: 0 0 auto; width: 300px; max-width: calc(100vw - 64px); min-height: 84px;
|
|
155
|
+
scroll-snap-align: center;
|
|
156
|
+
display: flex; align-items: center; gap: 10px; padding: 10px;
|
|
157
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
158
|
+
border: 2px solid transparent; border-radius: 14px;
|
|
159
|
+
box-shadow: var(--ui-shadow); cursor: pointer;
|
|
160
|
+
}
|
|
161
|
+
#carousel .card.active { border-color: var(--ui-text); }
|
|
162
|
+
/* Added once a photo loads (see loadPanelPhoto), after the text in the
|
|
163
|
+
DOM but shown first. */
|
|
164
|
+
#carousel .card-thumb {
|
|
165
|
+
order: -1; flex: 0 0 auto; width: 64px; height: 64px; border-radius: 10px;
|
|
101
166
|
background-size: cover; background-position: center;
|
|
102
167
|
}
|
|
103
|
-
/*
|
|
104
|
-
|
|
105
|
-
|
|
168
|
+
/* Until a photo loads, or when the place has none: the place's icon
|
|
169
|
+
on a muted tile, so every card has a picture. */
|
|
170
|
+
#carousel .card-thumb.empty {
|
|
171
|
+
background: var(--ui-surface-muted); color: var(--ui-icon-muted);
|
|
172
|
+
display: flex; align-items: center; justify-content: center;
|
|
173
|
+
}
|
|
174
|
+
#carousel .card-thumb.empty svg { width: 28px; height: 28px; display: block; }
|
|
175
|
+
/* Matches the numbered pin. */
|
|
176
|
+
#carousel .card-badge {
|
|
177
|
+
flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
|
|
106
178
|
background: #f97316; color: #1a0f04;
|
|
107
179
|
display: flex; align-items: center; justify-content: center;
|
|
108
|
-
font-size:
|
|
180
|
+
font-size: 11px; font-weight: 700;
|
|
109
181
|
}
|
|
110
|
-
#
|
|
111
|
-
#
|
|
182
|
+
#carousel .card-body { flex: 1 1 auto; min-width: 0; }
|
|
183
|
+
#carousel .card-name {
|
|
184
|
+
font-size: 14px; font-weight: 600;
|
|
185
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
186
|
+
}
|
|
187
|
+
#carousel .card-meta, #carousel .card-extra {
|
|
188
|
+
font-size: 12px; color: var(--ui-text-muted); margin-top: 3px;
|
|
189
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
190
|
+
}
|
|
191
|
+
#carousel .hours-open { color: #15803d; font-weight: 600; }
|
|
192
|
+
#carousel .hours-closed { color: #b91c1c; font-weight: 600; }
|
|
193
|
+
:root[data-theme="dark"] #carousel .hours-open { color: #4ade80; }
|
|
194
|
+
:root[data-theme="dark"] #carousel .hours-closed { color: #f87171; }
|
|
112
195
|
</style>
|
|
113
196
|
</head>
|
|
114
197
|
<body>
|
|
115
198
|
<div id="map"></div>
|
|
116
199
|
<div id="summary" style="display:none"></div>
|
|
117
|
-
<div id="
|
|
118
|
-
<div id="
|
|
200
|
+
<div id="legends">
|
|
201
|
+
<div id="legend" style="display:none"></div>
|
|
202
|
+
<div id="traffic-legend" style="display:none"></div>
|
|
203
|
+
</div>
|
|
204
|
+
<div id="carousel" style="display:none"></div>
|
|
119
205
|
<div id="loading">Loading…</div>
|
|
120
206
|
<div id="error" style="display:none"></div>
|
|
121
207
|
${initialDataScript}
|
|
@@ -129,9 +215,11 @@ ${initialDataScript}
|
|
|
129
215
|
var errorEl = document.getElementById('error');
|
|
130
216
|
var summaryEl = document.getElementById('summary');
|
|
131
217
|
var legendEl = document.getElementById('legend');
|
|
132
|
-
var
|
|
133
|
-
|
|
134
|
-
|
|
218
|
+
var trafficLegendEl = document.getElementById('traffic-legend');
|
|
219
|
+
var legendsEl = document.getElementById('legends');
|
|
220
|
+
var carouselEl = document.getElementById('carousel');
|
|
221
|
+
// Populated by buildSearchMiniPayload's marker tagging so the carousel (card
|
|
222
|
+
// <-> pin) and Place Details enrichment can find a marker by mapbox_id
|
|
135
223
|
// without re-querying the map for it.
|
|
136
224
|
var markersById = {};
|
|
137
225
|
|
|
@@ -148,6 +236,45 @@ ${initialDataScript}
|
|
|
148
236
|
var trackedLayerIds = [];
|
|
149
237
|
var trackedSourceIds = [];
|
|
150
238
|
var trackedMarkers = [];
|
|
239
|
+
// Copies of the GeoJSON layers drawn so far. setStyle() drops every
|
|
240
|
+
// source and layer we added (DOM markers survive it), so a style switch
|
|
241
|
+
// from the Satellite button puts these back (see restoreOverlays).
|
|
242
|
+
var drawnLayers = [];
|
|
243
|
+
var currentBaseMapConfig = null;
|
|
244
|
+
var payloadLegend = [];
|
|
245
|
+
var trafficOn = false;
|
|
246
|
+
// Mapbox Traffic v1: live congestion per road segment, refreshed every few
|
|
247
|
+
// minutes. Served from api.mapbox.com, which the page's CSP already allows.
|
|
248
|
+
var TRAFFIC_ID = 'mapbox-live-traffic';
|
|
249
|
+
// Colors from the design's traffic legend (Fast to Slow).
|
|
250
|
+
var TRAFFIC_LEVELS = [
|
|
251
|
+
{ value: 'low', label: 'Light traffic', color: '#00c94e' },
|
|
252
|
+
{ value: 'moderate', label: 'Moderate traffic', color: '#ff9900' },
|
|
253
|
+
{ value: 'heavy', label: 'Heavy traffic', color: '#ff2b3a' },
|
|
254
|
+
{ value: 'severe', label: 'Severe traffic', color: '#8b0a18' }
|
|
255
|
+
];
|
|
256
|
+
var TRAFFIC_MAIN_ROADS = [
|
|
257
|
+
'motorway', 'motorway_link', 'trunk', 'trunk_link',
|
|
258
|
+
'primary', 'secondary', 'tertiary', 'link'
|
|
259
|
+
];
|
|
260
|
+
// 24px filled icons (a globe and a car, as in the design), sized to match
|
|
261
|
+
// the zoom buttons. The globe's lines are cut out in the button color.
|
|
262
|
+
var TOGGLE_ICONS = {
|
|
263
|
+
satellite: '<circle cx="12" cy="12" r="9.5"/>' +
|
|
264
|
+
'<path d="M2.5 12h19M12 2.5a14 14 0 0 1 0 19M12 2.5a14 14 0 0 0 0 19" ' +
|
|
265
|
+
'fill="none" stroke="#fff" stroke-width="1.6"/>',
|
|
266
|
+
traffic: '<path fill-rule="evenodd" d="M5 11l1.6-4.4A2.4 2.4 0 0 1 8.9 5h6.2' +
|
|
267
|
+
'a2.4 2.4 0 0 1 2.3 1.6L19 11a2 2 0 0 1 2 2v4a1 1 0 0 1-1 1h-1v1.5' +
|
|
268
|
+
'a1.5 1.5 0 0 1-3 0V18H8v1.5a1.5 1.5 0 0 1-3 0V18H4a1 1 0 0 1-1-1v-4' +
|
|
269
|
+
'a2 2 0 0 1 2-2zm2.2 0h9.6l-1.1-3.2a1 1 0 0 0-1-.8H9.3a1 1 0 0 0-1 .8z' +
|
|
270
|
+
'M7.5 13a1.5 1.5 0 1 0 0 3a1.5 1.5 0 1 0 0-3zm9 0a1.5 1.5 0 1 0 0 3' +
|
|
271
|
+
'a1.5 1.5 0 1 0 0-3z"/>'
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Set by the layer toggle control, which initMap() adds before the rest
|
|
275
|
+
// of this script has run.
|
|
276
|
+
var satelliteButton = null;
|
|
277
|
+
var trafficButton = null;
|
|
151
278
|
|
|
152
279
|
// --- MCP App postMessage protocol -----------------------------------------
|
|
153
280
|
var messageId = 0;
|
|
@@ -186,6 +313,7 @@ ${initialDataScript}
|
|
|
186
313
|
}
|
|
187
314
|
|
|
188
315
|
window.addEventListener('message', function(event) {
|
|
316
|
+
if (event.source !== window.parent) return;
|
|
189
317
|
var message = event.data;
|
|
190
318
|
if (!message || typeof message !== 'object') return;
|
|
191
319
|
if (message.id !== undefined && pendingRequests.has(message.id)) {
|
|
@@ -199,12 +327,56 @@ ${initialDataScript}
|
|
|
199
327
|
handleToolResult(message.params);
|
|
200
328
|
}
|
|
201
329
|
if (message.method === 'ui/notifications/host-context-changed' &&
|
|
202
|
-
message.params
|
|
203
|
-
|
|
204
|
-
|
|
330
|
+
message.params) {
|
|
331
|
+
// Partial updates: only the fields that changed are sent.
|
|
332
|
+
applyHostContext(message.params);
|
|
333
|
+
if (message.params.displayMode) {
|
|
334
|
+
currentDisplayMode = message.params.displayMode;
|
|
335
|
+
if (map) setTimeout(function() { map.resize(); }, 100);
|
|
336
|
+
}
|
|
205
337
|
}
|
|
206
338
|
});
|
|
207
339
|
|
|
340
|
+
// --- Light/dark theme -----------------------------------------------------
|
|
341
|
+
var rootEl = document.documentElement;
|
|
342
|
+
var hostTheme = null;
|
|
343
|
+
|
|
344
|
+
function applyTheme(theme) {
|
|
345
|
+
if ((theme !== 'light' && theme !== 'dark') || !rootEl) return;
|
|
346
|
+
rootEl.setAttribute('data-theme', theme);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// The host's theme wins over the system's; its style variables
|
|
350
|
+
// (--color-background-primary and so on) feed the --ui-* colors.
|
|
351
|
+
function applyHostContext(ctx) {
|
|
352
|
+
if (!ctx || typeof ctx !== 'object') return;
|
|
353
|
+
if (ctx.theme === 'light' || ctx.theme === 'dark') {
|
|
354
|
+
hostTheme = ctx.theme;
|
|
355
|
+
applyTheme(hostTheme);
|
|
356
|
+
}
|
|
357
|
+
var vars = ctx.styles && ctx.styles.variables;
|
|
358
|
+
if (vars && typeof vars === 'object' && rootEl && rootEl.style &&
|
|
359
|
+
typeof rootEl.style.setProperty === 'function') {
|
|
360
|
+
Object.keys(vars).forEach(function(name) {
|
|
361
|
+
if (/^--[a-z0-9-]+$/i.test(name) && typeof vars[name] === 'string') {
|
|
362
|
+
rootEl.style.setProperty(name, vars[name]);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
window.__applyHostContext = applyHostContext;
|
|
368
|
+
|
|
369
|
+
var darkQuery = typeof window.matchMedia === 'function'
|
|
370
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
371
|
+
: null;
|
|
372
|
+
function followSystemTheme() {
|
|
373
|
+
if (!hostTheme && darkQuery) applyTheme(darkQuery.matches ? 'dark' : 'light');
|
|
374
|
+
}
|
|
375
|
+
followSystemTheme();
|
|
376
|
+
if (darkQuery && typeof darkQuery.addEventListener === 'function') {
|
|
377
|
+
darkQuery.addEventListener('change', followSystemTheme);
|
|
378
|
+
}
|
|
379
|
+
|
|
208
380
|
// Set once a host answers ui/initialize, even with an error. A page from
|
|
209
381
|
// export_map_html_tool framed by some other site never gets an answer.
|
|
210
382
|
var hostAnswered = false;
|
|
@@ -213,8 +385,9 @@ ${initialDataScript}
|
|
|
213
385
|
appCapabilities: {},
|
|
214
386
|
appInfo: { name: 'Mapbox Map', version: '1.0.0' }
|
|
215
387
|
}).then(
|
|
216
|
-
function() {
|
|
388
|
+
function(result) {
|
|
217
389
|
hostAnswered = true;
|
|
390
|
+
if (result) applyHostContext(result.hostContext);
|
|
218
391
|
sendNotification('ui/notifications/initialized', {});
|
|
219
392
|
},
|
|
220
393
|
function() {
|
|
@@ -269,6 +442,7 @@ ${initialDataScript}
|
|
|
269
442
|
}
|
|
270
443
|
map = new mapboxgl.Map(mapOptions);
|
|
271
444
|
map.addControl(new mapboxgl.NavigationControl(), 'top-left');
|
|
445
|
+
map.addControl(new LayerToggleControl(), 'top-left');
|
|
272
446
|
map.on('load', function() {
|
|
273
447
|
mapLoaded = true;
|
|
274
448
|
if (pendingPayload) {
|
|
@@ -401,6 +575,7 @@ ${initialDataScript}
|
|
|
401
575
|
} else {
|
|
402
576
|
summaryEl.style.display = 'none';
|
|
403
577
|
}
|
|
578
|
+
placeTopLeftControls();
|
|
404
579
|
if (!map) { loadingEl.style.display = 'none'; return; }
|
|
405
580
|
if (mapLoaded) render(payload);
|
|
406
581
|
else pendingPayload = payload;
|
|
@@ -424,8 +599,13 @@ ${initialDataScript}
|
|
|
424
599
|
trackedLayerIds = [];
|
|
425
600
|
trackedSourceIds = [];
|
|
426
601
|
trackedMarkers = [];
|
|
602
|
+
drawnLayers = [];
|
|
603
|
+
// A render without baseMapConfig must not bring back the last one (say,
|
|
604
|
+
// night mode) when the Satellite button switches style.
|
|
605
|
+
currentBaseMapConfig = null;
|
|
427
606
|
markersById = {};
|
|
428
|
-
|
|
607
|
+
carouselEl.style.display = 'none';
|
|
608
|
+
legendsEl.style.bottom = '';
|
|
429
609
|
clearPanel();
|
|
430
610
|
// Place Details responses still in flight belong to the old render.
|
|
431
611
|
panelGeneration += 1;
|
|
@@ -461,10 +641,105 @@ ${initialDataScript}
|
|
|
461
641
|
end: { color: '#ef4444' }
|
|
462
642
|
};
|
|
463
643
|
|
|
644
|
+
// Maki icons (https://github.com/mapbox/maki, CC0), 15x15 path data, for
|
|
645
|
+
// the categories search results most often have. Anything else gets
|
|
646
|
+
// the plain marker.
|
|
647
|
+
var MAKI = {
|
|
648
|
+
'attraction': 'M6,2C5.446,2,5.2478,2.5045,5,3L4.5,4h-2C1.669,4,1,4.669,1,5.5v5C1,11.331,1.669,12,2.5,12h10 c0.831,0,1.5-0.669,1.5-1.5v-5C14,4.669,13.331,4,12.5,4h-2L10,3C9.75,2.5,9.554,2,9,2H6z M2.5,5C2.7761,5,3,5.2239,3,5.5 S2.7761,6,2.5,6S2,5.7761,2,5.5S2.2239,5,2.5,5z M7.5,5c1.6569,0,3,1.3431,3,3s-1.3431,3-3,3s-3-1.3431-3-3S5.8431,5,7.5,5z M7.5,6.5C6.6716,6.5,6,7.1716,6,8l0,0c0,0.8284,0.6716,1.5,1.5,1.5l0,0C8.3284,9.5,9,8.8284,9,8l0,0C9,7.1716,8.3284,6.5,7.5,6.5 L7.5,6.5z',
|
|
649
|
+
'bakery': 'M5.294,4.382,6,9.5a.979.979,0,0,0,1,1H8a.979.979,0,0,0,1-1l.706-5.118C9.706,3,7.5,3,7.5,3S5.291,3,5.294,4.382ZM3.5,5C2,5,2,6,2,6l1,4H4.5a.793.793,0,0,0,.794-.765L4.5,5Zm-2,2.5a1.533,1.533,0,0,0-1.059.412A1.366,1.366,0,0,0,0,8.794V11H.882A1.02,1.02,0,0,0,2,10ZM11.5,5C13,5,13,6,13,6l-1,4H10.5a.793.793,0,0,1-.794-.765L10.5,5Zm2,2.5a1.533,1.533,0,0,1,1.059.412A1.366,1.366,0,0,1,15,8.794V11h-.882A1.02,1.02,0,0,1,13,10Z',
|
|
650
|
+
'bank': 'M1,3C0.446,3,0,3.446,0,4v7c0,0.554,0.446,1,1,1h13c0.554,0,1-0.446,1-1V4c0-0.554-0.446-1-1-1H1z M1,4h1.5 C2.7761,4,3,4.2239,3,4.5S2.7761,5,2.5,5S2,4.7761,2,4.5L1.5,5C1.7761,5,2,5.2239,2,5.5S1.7761,6,1.5,6S1,5.7761,1,5.5V4z M7.5,4 C8.8807,4,10,5.567,10,7.5l0,0C10,9.433,8.8807,11,7.5,11S5,9.433,5,7.5S6.1193,4,7.5,4z M12.5,4H14v1.5C14,5.7761,13.7761,6,13.5,6 S13,5.7761,13,5.5S13.2239,5,13.5,5L13,4.5C13,4.7761,12.7761,5,12.5,5S12,4.7761,12,4.5S12.2239,4,12.5,4z M7.5,5.5 c-0.323,0-0.5336,0.1088-0.6816,0.25h1.3633C8.0336,5.6088,7.823,5.5,7.5,5.5z M6.625,6C6.5795,6.091,6.5633,6.1711,6.5449,6.25 h1.9102C8.4367,6.1711,8.4205,6.091,8.375,6H6.625z M6.5,6.5v0.25h2V6.5H6.5z M6.5,7v0.25h2V7H6.5z M6.5,7.5v0.25h2V7.5H6.5z M6.5,8 L6.25,8.25h2L8.5,8H6.5z M6,8.5c0,0,0.0353,0.1024,0.1016,0.25H8.375L8,8.5H6z M1.5,9C1.7761,9,2,9.2239,2,9.5S1.7761,10,1.5,10 L2,10.5C2,10.2239,2.2239,10,2.5,10S3,10.2239,3,10.5S2.7761,11,2.5,11H1V9.5C1,9.2239,1.2239,9,1.5,9z M6.2383,9 C6.2842,9.0856,6.3144,9.159,6.375,9.25h2.2676C8.7092,9.1121,8.75,9,8.75,9H6.2383z M13.5,9C13.7761,9,14,9.2239,14,9.5V11h-1.5 c-0.2761,0-0.5-0.2239-0.5-0.5s0.2239-0.5,0.5-0.5s0.5,0.2239,0.5,0.5l0.5-0.5C13.2239,10,13,9.7761,13,9.5S13.2239,9,13.5,9z M6.5664,9.5c0.0786,0.0912,0.1647,0.1763,0.2598,0.25h1.4199C8.3462,9.6727,8.4338,9.5883,8.5,9.5H6.5664z',
|
|
651
|
+
'bar': 'M7.5,1c-2,0-7,0.25-6.5,0.75L7,8v4 c0,1-3,0.5-3,2h7c0-1.5-3-1-3-2V8l6-6.25C14.5,1.25,9.5,1,7.5,1z M7.5,2c2.5,0,4.75,0.25,4.75,0.25L11.5,3h-8L2.75,2.25 C2.75,2.25,5,2,7.5,2z',
|
|
652
|
+
'beer': 'M12,5V2c0,0-1-1-4.5-1S3,2,3,2v3c0.0288,1.3915,0.3706,2.7586,1,4c0.6255,1.4348,0.6255,3.0652,0,4.5c0,0,0,1,3.5,1 s3.5-1,3.5-1c-0.6255-1.4348-0.6255-3.0652,0-4.5C11.6294,7.7586,11.9712,6.3915,12,5z M7.5,13.5 c-0.7966,0.035-1.5937-0.0596-2.36-0.28c0.203-0.7224,0.304-1.4696,0.3-2.22h4.12c-0.004,0.7504,0.097,1.4976,0.3,2.22 C9.0937,13.4404,8.2966,13.535,7.5,13.5z M7.5,5C6.3136,5.0299,5.1306,4.8609,4,4.5v-2C5.131,2.1411,6.3137,1.9722,7.5,2 C8.6863,1.9722,9.869,2.1411,11,2.5v2C9.8694,4.8609,8.6864,5.0299,7.5,5z',
|
|
653
|
+
'cafe': 'M12,5h-2V3H2v4c0.0133,2.2091,1.8149,3.9891,4.024,3.9758C7.4345,10.9673,8.7362,10.2166,9.45,9H12c1.1046,0,2-0.8954,2-2 S13.1046,5,12,5z M12,8H9.86C9.9487,7.6739,9.9958,7.3379,10,7V6h2c0.5523,0,1,0.4477,1,1S12.5523,8,12,8z M10,12.5 c0,0.2761-0.2239,0.5-0.5,0.5h-7C2.2239,13,2,12.7761,2,12.5S2.2239,12,2.5,12h7C9.7761,12,10,12.2239,10,12.5z',
|
|
654
|
+
'cinema': 'M14,7.5v2c0,0.2761-0.2239,0.5-0.5,0.5S13,9.7761,13,9.5c0,0,0.06-0.5-1-0.5h-1v2.5c0,0.2761-0.2239,0.5-0.5,0.5h-8 C2.2239,12,2,11.7761,2,11.5v-4C2,7.2239,2.2239,7,2.5,7h8C10.7761,7,11,7.2239,11,7.5V8h1c1.06,0,1-0.5,1-0.5 C13,7.2239,13.2239,7,13.5,7S14,7.2239,14,7.5z M4,3C2.8954,3,2,3.8954,2,5s0.8954,2,2,2s2-0.8954,2-2S5.1046,3,4,3z M4,6 C3.4477,6,3,5.5523,3,5s0.4477-1,1-1s1,0.4477,1,1S4.5523,6,4,6z M8.5,2C7.1193,2,6,3.1193,6,4.5S7.1193,7,8.5,7S11,5.8807,11,4.5 S9.8807,2,8.5,2z M8.5,6C7.6716,6,7,5.3284,7,4.5S7.6716,3,8.5,3S10,3.6716,10,4.5S9.3284,6,8.5,6z',
|
|
655
|
+
'convenience': 'M2.9982,3.0687s.1875-1.062,1-1.062H5.0043V1.7276a.5.5,0,0,1,.5-.5H9.4882a.5.5,0,0,1,.5.5v.2791h1.01a.8324.8324,0,0,1,.6373.2937,1.7589,1.7589,0,0,1,.3627.7683s.3108,1.5719.5921,2.9181h-.938L11,3,3.982,2.9956,3.3336,5.9868H2.382Zm11,4.9144a1,1,0,0,1-.9992,1l-1.0008,3.973s-.1875,1.0417-1,1.0417h-7c-.8125,0-1-1.0417-1-1.0417L1.9822,8.98a.9985.9985,0,0,1,.016-1.9968h11A1,1,0,0,1,13.9982,7.9831ZM5.0327,12.13,5.03,11.0051,4.6392,9.4686a.5373.5373,0,1,0-1.0649.1445L3.78,11.0075l.1874,1.2672A.5373.5373,0,1,0,5.0327,12.13Zm2-2.5967A.5332.5332,0,0,0,6.4994,9l-.0039.0008L6.4886,9a.4992.4992,0,0,0-.3575.1509.529.529,0,0,0-.1648.3828v2.68a.5332.5332,0,1,0,1.0663,0Zm1.998,0A.5314.5314,0,0,0,8.694,9.04a.4992.4992,0,0,0-.393,0,.5312.5312,0,0,0-.3366.4934v2.68a.5331.5331,0,1,0,1.0662,0Zm2.3857.08a.5373.5373,0,1,0-1.0649-.1445L9.96,10.9938,9.9579,12.13a.5373.5373,0,1,0,1.0649.1446Z',
|
|
656
|
+
'fast-food': 'M14,8c0,0.5523-0.4477,1-1,1H2C1.4477,9,1,8.5523,1,8s0.4477-1,1-1h11C13.5523,7,14,7.4477,14,8z M3.5,10H2 c0,1.6569,1.3431,3,3,3h5c1.6569,0,3-1.3431,3-3H3.5z M3,6H2V4c0-1.1046,0.8954-2,2-2h7c1.1046,0,2,0.8954,2,2v2H3z M11,4.5 C11,4.7761,11.2239,5,11.5,5S12,4.7761,12,4.5S11.7761,4,11.5,4S11,4.2239,11,4.5z M9,3.5C9,3.7761,9.2239,4,9.5,4S10,3.7761,10,3.5 S9.7761,3,9.5,3S9,3.2239,9,3.5z M7,4.5C7,4.7761,7.2239,5,7.5,5S8,4.7761,8,4.5S7.7761,4,7.5,4S7,4.2239,7,4.5z M5,3.5 C5,3.7761,5.2239,4,5.5,4S6,3.7761,6,3.5S5.7761,3,5.5,3S5,3.2239,5,3.5z M3,4.5C3,4.7761,3.2239,5,3.5,5S4,4.7761,4,4.5 S3.7761,4,3.5,4S3,4.2239,3,4.5z',
|
|
657
|
+
'fuel': 'm14 6v5.5c0 .2761-.2239.5-.5.5s-.5-.2239-.5-.5v-2c0-.8284-.6716-1.5-1.5-1.5h-1.5v-6c0-.5523-.4477-1-1-1h-6c-.5523 0-1 .4477-1 1v11c0 .5523.4477 1 1 1h6c.5523 0 1-.4477 1-1v-4h1.5c.2761 0 .5.2239.5.5v2c0 .8284.6716 1.5 1.5 1.5s1.5-.6716 1.5-1.5v-6.5c0-.5523-.4477-1-1-1v-1.51c-.0054-.2722-.2277-.4901-.5-.49-.2816.0047-.5062.2367-.5015.5184.0002.0105.0007.0211.0015.0316v2.45c0 .5523.4477 1 1 1s1-.4477 1-1-.4477-1-1-1zm-5 .5c0 .2761-.2239.5-.5.5h-5c-.2761 0-.5-.2239-.5-.5v-3c0-.2761.2239-.5.5-.5h5c.2761 0 .5.2239.5.5z',
|
|
658
|
+
'grocery': 'M 13.199219 1.5 C 13.199219 1.5 11.808806 1.4588 11.253906 2 C 10.720406 2.5202 10.5 2.9177 10.5 4 L 1.1992188 4 L 2.59375 8.8144531 C 2.59725 8.8217531 2.6036219 8.8287375 2.6074219 8.8359375 C 2.8418219 9.4932375 3.4545469 9.9666406 4.1855469 9.9941406 C 4.1885469 9.9954406 4.1992187 10 4.1992188 10 L 10.699219 10 L 10.699219 10.199219 C 10.699219 10.199219 10.7 10.500391 10.5 10.900391 C 10.3 11.300391 10.200391 11.5 9.4003906 11.5 L 2.9003906 11.5 C 1.9003906 11.5 1.9003906 13 2.9003906 13 L 4.0996094 13 L 4.1992188 13 L 9.0996094 13 L 9.1992188 13 L 9.3007812 13 C 10.500781 13 11.399219 12.299609 11.699219 11.599609 C 11.999219 10.899609 12 10.300781 12 10.300781 L 12 10 L 12 4 C 12 3.4764 12.228619 3 12.699219 3 L 13.25 3 C 13.6642 3 14 2.6642 14 2.25 C 14 1.8358 13.6642 1.5 13.25 1.5 L 13.199219 1.5 z M 9.1992188 13 C 8.5992188 13 8.1992188 13.4 8.1992188 14 C 8.1992188 14.6 8.5992187 15 9.1992188 15 C 9.7992187 15 10.199219 14.6 10.199219 14 C 10.199219 13.4 9.7992188 13 9.1992188 13 z M 4.1992188 13 C 3.5992188 13 3.1992188 13.4 3.1992188 14 C 3.1992188 14.6 3.5992187 15 4.1992188 15 C 4.7992188 15 5.1992188 14.6 5.1992188 14 C 5.1992188 13.4 4.7992187 13 4.1992188 13 z',
|
|
659
|
+
'hospital': 'M7,1C6.4,1,6,1.4,6,2v4H2C1.4,6,1,6.4,1,7v1 c0,0.6,0.4,1,1,1h4v4c0,0.6,0.4,1,1,1h1c0.6,0,1-0.4,1-1V9h4c0.6,0,1-0.4,1-1V7c0-0.6-0.4-1-1-1H9V2c0-0.6-0.4-1-1-1H7z',
|
|
660
|
+
'ice-cream': 'M5.44,8.17a3.473,3.473,0,0,0,2-.63A3.5,3.5,0,0,0,9,8.14h.44L8,13.7a.5.5,0,0,1-.92,0Zm6-3.473a2,2,0,0,1-4,0h0a2,2,0,1,1-2-2h.12a2,2,0,1,1,3.75,0h.13A2,2,0,0,1,11.439,4.7Z',
|
|
661
|
+
'lodging': 'M0.5,2.5C0.2,2.5,0,2.7,0,3v7.5v2C0,12.8,0.2,13,0.5,13S1,12.8,1,12.5V11h13v1.5 c0,0.3,0.2,0.5,0.5,0.5s0.5-0.2,0.5-0.5v-2c0-0.3-0.2-0.5-0.5-0.5H1V3C1,2.7,0.8,2.5,0.5,2.5z M3.5,3C2.7,3,2,3.7,2,4.5l0,0 C2,5.3,2.7,6,3.5,6l0,0C4.3,6,5,5.3,5,4.5l0,0C5,3.7,4.3,3,3.5,3L3.5,3z M7,4C5.5,4,5.5,5.5,5.5,5.5V7h-3C2.2,7,2,7.2,2,7.5v1 C2,8.8,2.2,9,2.5,9H6h9V6.5C15,4,12.5,4,12.5,4H7z',
|
|
662
|
+
'marker': 'M7.5 1C5.42312 1 3 2.2883 3 5.56759C3 7.79276 6.46156 12.7117 7.5 14C8.42309 12.7117 12 7.90993 12 5.56759C12 2.2883 9.57688 1 7.5 1Z',
|
|
663
|
+
'museum': 'M7.5,0L1,3.5V4h13V3.5L7.5,0z M2,5v5l-1,1.6V13h13v-1.4L13,10V5H2z M4,6h1v5.5H4V6z M7,6h1v5.5H7V6z M10,6h1 v5.5h-1V6z',
|
|
664
|
+
'park': 'M14,5.75c0.0113-0.6863-0.3798-1.3159-1-1.61C12.9475,3.4906,12.4014,2.9926,11.75,3 c-0.0988,0.0079-0.1962,0.0281-0.29,0.06c-0.0607-0.66-0.6449-1.1458-1.3048-1.0851C9.8965,1.9987,9.6526,2.1058,9.46,2.28l0,0 c0-0.6904-0.5596-1.25-1.25-1.25S6.96,1.5896,6.96,2.28C6.96,2.28,7,2.3,7,2.33C6.4886,1.8913,5.7184,1.9503,5.2797,2.4618 C5.1316,2.6345,5.0347,2.8451,5,3.07C4.8417,3.0195,4.6761,2.9959,4.51,3C3.6816,2.9931,3.0044,3.659,2.9975,4.4874 C2.9958,4.6872,3.0341,4.8852,3.11,5.07C2.3175,5.2915,1.8546,6.1136,2.0761,6.9061C2.2163,7.4078,2.6083,7.7998,3.11,7.94 c0.2533,0.7829,1.0934,1.2123,1.8763,0.959C5.5216,8.7258,5.9137,8.2659,6,7.71C6.183,7.8691,6.4093,7.9701,6.65,8v5L5,14h5l-1.6-1 v-2c0.7381-0.8915,1.6915-1.5799,2.77-2c0.8012,0.1879,1.603-0.3092,1.7909-1.1103C12.9893,7.7686,13.0025,7.6444,13,7.52 c0.0029-0.0533,0.0029-0.1067,0-0.16C13.6202,7.0659,14.0113,6.4363,14,5.75z M8.4,10.26V6.82C8.6703,7.3007,9.1785,7.5987,9.73,7.6 h0.28c0.0156,0.4391,0.2242,0.849,0.57,1.12C9.7643,9.094,9.0251,9.6162,8.4,10.26z',
|
|
665
|
+
'parking': 'M4 2V13H6V9H8.5C10.433 9 12 7.433 12 5.5C12 3.567 10.433 2 8.5 2H4ZM6 7V4H8.5C9.32843 4 10 4.67157 10 5.5C10 6.32843 9.32843 7 8.5 7H6Z',
|
|
666
|
+
'pharmacy': 'M9.5,4l1.07-1.54c0.0599,0.0046,0.1201,0.0046,0.18,0c0.6904-0.0004,1.2497-0.5603,1.2494-1.2506 C11.999,0.519,11.4391-0.0404,10.7487-0.04C10.0584-0.0396,9.499,0.5203,9.4994,1.2106c0,0.0131,0.0002,0.0262,0.0006,0.0394 c0,0,0,0.07,0,0.1L7,4H9.5z M12,6V5H3v1l1.5,3.5L3,13v1h9v-1l-1-3.5L12,6z M10,10H8v2H7v-2H5V9h2V7h1v2h2V10z',
|
|
667
|
+
'restaurant-pizza': 'M1.393 7.931.025 7.315A14.05 14.05 0 0 1 7.764-.001l.027.07.511 1.331-.003.001a12.613 12.613 0 0 0-6.906 6.53Zm10.963 4.96L2.42 8.414a11.604 11.604 0 0 1 6.354-6.009l.003-.001.817 2.122c-.028-.002-.052-.016-.081-.016a1.062 1.062 0 1 0 .764 1.793l2.413 6.273a.249.249 0 0 1-.334.316ZM7.426 7.52a.925.925 0 1 0-1.849 0 .925.925 0 0 0 1.849 0Zm3.017 2.03a.925.925 0 1 0-.07.353.924.924 0 0 0 .07-.354Z',
|
|
668
|
+
'restaurant': 'M3.5,0l-1,5.5c-0.1464,0.805,1.7815,1.181,1.75,2L4,14c-0.0384,0.9993,1,1,1,1s1.0384-0.0007,1-1L5.75,7.5 c-0.0314-0.8176,1.7334-1.1808,1.75-2L6.5,0H6l0.25,4L5.5,4.5L5.25,0h-0.5L4.5,4.5L3.75,4L4,0H3.5z M12,0 c-0.7364,0-1.9642,0.6549-2.4551,1.6367C9.1358,2.3731,9,4.0182,9,5v2.5c0,0.8182,1.0909,1,1.5,1L10,14c-0.0905,0.9959,1,1,1,1 s1,0,1-1V0z',
|
|
669
|
+
'shop': 'm13.33 5h-1.83l-.39-2.33c-.1601-.7182-.7017-1.2905-1.41-1.49-.3493-.1124-.7131-.173-1.08-.18h-2.24c-.3669.007-.7307.0676-1.08.18-.7083.1995-1.2499.7718-1.41 1.49l-.39 2.33h-1.83c-.2761-.0017-.5013.2208-.503.497-.0003.0519.0074.1035.023.153l1.88 6.3c.1964.6246.7753 1.0496 1.43 1.05h6c.651-.0047 1.2247-.4289 1.42-1.05l1.88-6.3c.0829-.2634-.0635-.5441-.3269-.627-.0463-.0146-.0945-.0223-.1431-.023zm-8.81 0 .36-2.17c.0807-.3625.3736-.6395.74-.7.2463-.0776.5019-.1213.76-.13h2.24c.2614.0078.5205.0515.77.13.3664.0605.6593.3375.74.7l.35 2.17h-6z',
|
|
670
|
+
'theatre': 'M2,1c0,0-1,0-1,1v5.1582C1,8.8885,1.354,11,4.5,11H5V8L2.5,9c0,0,0-2.5,2.5-2.5V5 c0-0.7078,0.0868-1.3209,0.5-1.7754C5.8815,2.805,6.5046,1.9674,8.1562,2.7539L9,3.3027V2c0,0,0-1-1-1C7.2922,1,6.0224,2,5,2 S2.7865,1,2,1z M3,3c0.5523,0,1,0.4477,1,1S3.5523,5,3,5S2,4.5523,2,4S2.4477,3,3,3z M7,4c0,0-1,0-1,1v5c0,2,1,4,4,4s4-2,4-4V5 c0-1-1-1-1-1c-0.7078,0-1.9776,1-3,1S7.7865,4,7,4z M8,6c0.5523,0,1,0.4477,1,1S8.5523,8,8,8S7,7.5523,7,7S7.4477,6,8,6z M12,6 c0.5523,0,1,0.4477,1,1s-0.4477,1-1,1s-1-0.4477-1-1S11.4477,6,12,6z M7.5,10H10h2.5c0,0,0,2.5-2.5,2.5S7.5,10,7.5,10z'
|
|
671
|
+
};
|
|
672
|
+
// Category words (Search Box poi_category, or a caller's category) to
|
|
673
|
+
// Maki names, checked in order, so "pizza restaurant" is a pizza.
|
|
674
|
+
var CATEGORY_ICONS = [
|
|
675
|
+
['pizza', 'restaurant-pizza'], ['coffee', 'cafe'], ['cafe', 'cafe'],
|
|
676
|
+
['café', 'cafe'], ['bakery', 'bakery'], ['ice cream', 'ice-cream'],
|
|
677
|
+
['brewery', 'beer'], ['beer', 'beer'], ['pub', 'beer'],
|
|
678
|
+
['bar', 'bar'], ['nightclub', 'bar'], ['wine', 'bar'],
|
|
679
|
+
['fast food', 'fast-food'], ['burger', 'fast-food'],
|
|
680
|
+
['restaurant', 'restaurant'], ['food', 'restaurant'], ['diner', 'restaurant'],
|
|
681
|
+
['hotel', 'lodging'], ['motel', 'lodging'], ['hostel', 'lodging'], ['lodging', 'lodging'],
|
|
682
|
+
['grocery', 'grocery'], ['supermarket', 'grocery'], ['convenience', 'convenience'],
|
|
683
|
+
['gas', 'fuel'], ['fuel', 'fuel'], ['parking', 'parking'],
|
|
684
|
+
['hospital', 'hospital'], ['clinic', 'hospital'], ['pharmacy', 'pharmacy'],
|
|
685
|
+
['park', 'park'], ['garden', 'park'], ['museum', 'museum'], ['gallery', 'museum'],
|
|
686
|
+
['theater', 'theatre'], ['theatre', 'theatre'], ['cinema', 'cinema'], ['movie', 'cinema'],
|
|
687
|
+
['bank', 'bank'], ['atm', 'bank'],
|
|
688
|
+
['shop', 'shop'], ['store', 'shop'], ['boutique', 'shop'],
|
|
689
|
+
['attraction', 'attraction'], ['landmark', 'attraction']
|
|
690
|
+
];
|
|
691
|
+
|
|
692
|
+
// The Maki name for a marker or card: its own maki when we have that
|
|
693
|
+
// icon, else one matched from its category, else the plain marker.
|
|
694
|
+
function placeIcon(maki, category) {
|
|
695
|
+
if (typeof maki === 'string' && MAKI.hasOwnProperty(maki)) return maki;
|
|
696
|
+
if (typeof category === 'string') {
|
|
697
|
+
var words = ' ' + category.toLowerCase().split(/[^a-zà-ÿ]+/).join(' ') + ' ';
|
|
698
|
+
for (var i = 0; i < CATEGORY_ICONS.length; i++) {
|
|
699
|
+
if (words.indexOf(' ' + CATEGORY_ICONS[i][0] + ' ') !== -1) return CATEGORY_ICONS[i][1];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return 'marker';
|
|
703
|
+
}
|
|
704
|
+
window.__placeIcon = placeIcon;
|
|
705
|
+
|
|
706
|
+
// Only ever called with a key of MAKI, so the markup is all ours.
|
|
707
|
+
function iconSvg(name) {
|
|
708
|
+
var d = MAKI[MAKI.hasOwnProperty(name) ? name : 'marker'];
|
|
709
|
+
return '<svg viewBox="0 0 15 15" aria-hidden="true"><path fill="currentColor" d="' + d + '"/></svg>';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function buildPlaceElement(icon, color) {
|
|
713
|
+
var el = document.createElement('div');
|
|
714
|
+
el.className = 'marker-badge place';
|
|
715
|
+
// A caller's color (gray for "other results", say) tints the icon.
|
|
716
|
+
if (color) el.style.color = color;
|
|
717
|
+
el.innerHTML = iconSvg(icon);
|
|
718
|
+
return el;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Dark or white, whichever reads better on the badge color (WCAG
|
|
722
|
+
// contrast), so numbers stay legible on orange, green or gray pins.
|
|
723
|
+
var BADGE_DARK_TEXT = '#1a0f04';
|
|
724
|
+
function badgeTextColor(color) {
|
|
725
|
+
var hex = typeof color === 'string' ? color.replace('#', '') : '';
|
|
726
|
+
if (hex.length === 3) hex = hex.split('').map(function(c) { return c + c; }).join('');
|
|
727
|
+
if (!/^[0-9a-f]{6}/i.test(hex)) return '#fff';
|
|
728
|
+
var lum = [0, 2, 4].map(function(i) {
|
|
729
|
+
var c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
730
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
731
|
+
});
|
|
732
|
+
var l = 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
|
733
|
+
// Against white (L=1) vs. against the dark text (L about 0.0055).
|
|
734
|
+
return (1.05 / (l + 0.05)) >= ((l + 0.05) / 0.0555) ? '#fff' : BADGE_DARK_TEXT;
|
|
735
|
+
}
|
|
736
|
+
window.__badgeTextColor = badgeTextColor;
|
|
737
|
+
|
|
464
738
|
function buildBadgeElement(label, color) {
|
|
465
739
|
var el = document.createElement('div');
|
|
466
740
|
el.className = 'marker-badge';
|
|
467
741
|
el.style.background = color;
|
|
742
|
+
el.style.color = badgeTextColor(color);
|
|
468
743
|
el.textContent = label;
|
|
469
744
|
return el;
|
|
470
745
|
}
|
|
@@ -476,7 +751,24 @@ ${initialDataScript}
|
|
|
476
751
|
if (!layer || !layer.id || !layer.data) return;
|
|
477
752
|
map.addSource(layer.id, { type: 'geojson', data: layer.data });
|
|
478
753
|
trackedSourceIds.push(layer.id);
|
|
754
|
+
map.addLayer(layerDefinition(layer));
|
|
755
|
+
trackedLayerIds.push(layer.id);
|
|
756
|
+
drawnLayers.push({
|
|
757
|
+
id: layer.id, type: layer.type, data: layer.data,
|
|
758
|
+
paint: layer.paint, layout: layer.layout, slot: layer.slot
|
|
759
|
+
});
|
|
479
760
|
|
|
761
|
+
var feature = layer.data;
|
|
762
|
+
if (feature.type === 'FeatureCollection' && Array.isArray(feature.features)) {
|
|
763
|
+
feature.features.forEach(function(f) {
|
|
764
|
+
if (f && f.geometry) bbox.extend(f.geometry.coordinates);
|
|
765
|
+
});
|
|
766
|
+
} else if (feature.geometry) {
|
|
767
|
+
bbox.extend(feature.geometry.coordinates);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function layerDefinition(layer) {
|
|
480
772
|
var def = {
|
|
481
773
|
id: layer.id,
|
|
482
774
|
type: layer.type,
|
|
@@ -485,17 +777,176 @@ ${initialDataScript}
|
|
|
485
777
|
if (layer.paint) def.paint = layer.paint;
|
|
486
778
|
if (layer.layout) def.layout = layer.layout;
|
|
487
779
|
if (layer.slot) def.slot = layer.slot;
|
|
488
|
-
|
|
489
|
-
|
|
780
|
+
return def;
|
|
781
|
+
}
|
|
490
782
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
783
|
+
// --- Satellite / Traffic toggles -----------------------------------------
|
|
784
|
+
function addTrafficLayer() {
|
|
785
|
+
if (!map.getSource(TRAFFIC_ID)) {
|
|
786
|
+
map.addSource(TRAFFIC_ID, {
|
|
787
|
+
type: 'vector',
|
|
788
|
+
url: 'mapbox://mapbox.mapbox-traffic-v1'
|
|
495
789
|
});
|
|
496
|
-
} else if (feature.geometry) {
|
|
497
|
-
bbox.extend(feature.geometry.coordinates);
|
|
498
790
|
}
|
|
791
|
+
if (map.getLayer(TRAFFIC_ID)) return;
|
|
792
|
+
var colors = ['match', ['get', 'congestion']];
|
|
793
|
+
TRAFFIC_LEVELS.forEach(function(level) { colors.push(level.value, level.color); });
|
|
794
|
+
colors.push('rgba(0,0,0,0)');
|
|
795
|
+
map.addLayer({
|
|
796
|
+
id: TRAFFIC_ID,
|
|
797
|
+
type: 'line',
|
|
798
|
+
source: TRAFFIC_ID,
|
|
799
|
+
'source-layer': 'traffic',
|
|
800
|
+
// Above land and water, below roads' labels and anything we draw.
|
|
801
|
+
slot: 'middle',
|
|
802
|
+
// Light traffic on every side street turns the whole map green, so
|
|
803
|
+
// show it only on main roads; any slowdown shows on every road.
|
|
804
|
+
filter: ['any',
|
|
805
|
+
['!=', ['get', 'congestion'], 'low'],
|
|
806
|
+
['match', ['get', 'class'], TRAFFIC_MAIN_ROADS, true, false]
|
|
807
|
+
],
|
|
808
|
+
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
809
|
+
paint: {
|
|
810
|
+
'line-color': colors,
|
|
811
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1, 15, 2, 18, 4],
|
|
812
|
+
// Each direction is its own line; shift it to its side of the road.
|
|
813
|
+
'line-offset': ['interpolate', ['linear'], ['zoom'], 12, 0.5, 15, 1.5, 18, 3],
|
|
814
|
+
'line-opacity': 0.9
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function removeTrafficLayer() {
|
|
820
|
+
if (map.getLayer(TRAFFIC_ID)) map.removeLayer(TRAFFIC_ID);
|
|
821
|
+
if (map.getSource(TRAFFIC_ID)) map.removeSource(TRAFFIC_ID);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function setTraffic(on) {
|
|
825
|
+
trafficOn = on;
|
|
826
|
+
if (map && mapLoaded) {
|
|
827
|
+
if (on) addTrafficLayer();
|
|
828
|
+
else removeTrafficLayer();
|
|
829
|
+
}
|
|
830
|
+
renderLegend();
|
|
831
|
+
syncToggleButtons();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Switches the base style, then runs afterLoad once the new style is up.
|
|
835
|
+
function switchBaseStyle(styleId, afterLoad) {
|
|
836
|
+
currentBaseStyleId = styleId;
|
|
837
|
+
syncToggleButtons();
|
|
838
|
+
map.setStyle('mapbox://styles/mapbox/' + styleId);
|
|
839
|
+
map.once('style.load', afterLoad);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// After a style switch from the Satellite button: put back what setStyle
|
|
843
|
+
// dropped, without refitting the camera the user may have moved.
|
|
844
|
+
function restoreOverlays() {
|
|
845
|
+
applyBaseMapConfig(currentBaseMapConfig);
|
|
846
|
+
drawnLayers.forEach(function(layer) {
|
|
847
|
+
if (!map.getSource(layer.id)) {
|
|
848
|
+
map.addSource(layer.id, { type: 'geojson', data: layer.data });
|
|
849
|
+
}
|
|
850
|
+
if (!map.getLayer(layer.id)) map.addLayer(layerDefinition(layer));
|
|
851
|
+
});
|
|
852
|
+
if (trafficOn) addTrafficLayer();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function toggleSatellite() {
|
|
856
|
+
if (!map || !mapLoaded) return;
|
|
857
|
+
switchBaseStyle(
|
|
858
|
+
currentBaseStyleId === 'standard-satellite' ? 'standard' : 'standard-satellite',
|
|
859
|
+
restoreOverlays
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// A GL JS control (onAdd returns its element). Built by a returned object
|
|
864
|
+
// rather than prototype methods because initMap() runs before the rest
|
|
865
|
+
// of this script, and addControl calls onAdd right away.
|
|
866
|
+
function LayerToggleControl() {
|
|
867
|
+
return { onAdd: buildLayerToggles, onRemove: function() {} };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function buildLayerToggles() {
|
|
871
|
+
var container = document.createElement('div');
|
|
872
|
+
container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group layer-toggles';
|
|
873
|
+
satelliteButton = buildToggleButton('Satellite view', TOGGLE_ICONS.satellite, toggleSatellite);
|
|
874
|
+
trafficButton = buildToggleButton('Live traffic', TOGGLE_ICONS.traffic, function() {
|
|
875
|
+
setTraffic(!trafficOn);
|
|
876
|
+
});
|
|
877
|
+
container.appendChild(satelliteButton);
|
|
878
|
+
container.appendChild(trafficButton);
|
|
879
|
+
syncToggleButtons();
|
|
880
|
+
return container;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function buildToggleButton(label, icon, onClick) {
|
|
884
|
+
var button = document.createElement('button');
|
|
885
|
+
button.type = 'button';
|
|
886
|
+
button.title = label;
|
|
887
|
+
if (typeof button.setAttribute === 'function') button.setAttribute('aria-label', label);
|
|
888
|
+
button.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" ' +
|
|
889
|
+
'fill="currentColor" aria-hidden="true">' + icon + '</svg>';
|
|
890
|
+
button.addEventListener('click', onClick);
|
|
891
|
+
return button;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function syncToggleButtons() {
|
|
895
|
+
setPressed(satelliteButton, currentBaseStyleId === 'standard-satellite');
|
|
896
|
+
setPressed(trafficButton, trafficOn);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function setPressed(button, pressed) {
|
|
900
|
+
if (!button) return;
|
|
901
|
+
button.className = pressed ? 'active' : '';
|
|
902
|
+
if (typeof button.setAttribute === 'function') {
|
|
903
|
+
button.setAttribute('aria-pressed', pressed ? 'true' : 'false');
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// The summary chip covers GL JS's top-left control corner, so move the
|
|
908
|
+
// zoom and layer buttons below it while it's shown.
|
|
909
|
+
function placeTopLeftControls() {
|
|
910
|
+
if (!map || typeof map.getContainer !== 'function') return;
|
|
911
|
+
var container = map.getContainer();
|
|
912
|
+
var corner = container && typeof container.querySelector === 'function'
|
|
913
|
+
? container.querySelector('.mapboxgl-ctrl-top-left')
|
|
914
|
+
: null;
|
|
915
|
+
if (!corner) return;
|
|
916
|
+
corner.style.top = summaryEl.style.display === 'block'
|
|
917
|
+
? (summaryEl.offsetHeight + 8) + 'px'
|
|
918
|
+
: '';
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// The payload's legend rows, and below them the traffic scale while
|
|
922
|
+
// traffic is on.
|
|
923
|
+
function renderLegend() {
|
|
924
|
+
renderTrafficLegend();
|
|
925
|
+
var rows = payloadLegend;
|
|
926
|
+
if (rows.length === 0) {
|
|
927
|
+
legendEl.style.display = 'none';
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
legendEl.innerHTML = rows.map(function(row) {
|
|
931
|
+
var op = (typeof row.opacity === 'number') ? row.opacity : 1;
|
|
932
|
+
return '<div class="row"><span class="swatch" style="background:' +
|
|
933
|
+
escapeAttr(row.color) + ';opacity:' + op + '"></span>' +
|
|
934
|
+
escapeText(row.label) + '</div>';
|
|
935
|
+
}).join('');
|
|
936
|
+
legendEl.style.display = 'block';
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function renderTrafficLegend() {
|
|
940
|
+
if (!trafficOn) {
|
|
941
|
+
trafficLegendEl.style.display = 'none';
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
trafficLegendEl.innerHTML = '<span>Fast</span><span class="bar">' +
|
|
945
|
+
TRAFFIC_LEVELS.map(function(level) {
|
|
946
|
+
return '<span title="' + level.label + '" style="background:' +
|
|
947
|
+
level.color + '"></span>';
|
|
948
|
+
}).join('') + '</span><span>Slow</span>';
|
|
949
|
+
trafficLegendEl.style.display = 'flex';
|
|
499
950
|
}
|
|
500
951
|
|
|
501
952
|
function addOneMarker(m, bbox) {
|
|
@@ -506,38 +957,58 @@ ${initialDataScript}
|
|
|
506
957
|
var color = m.color ||
|
|
507
958
|
(STYLE_DEFAULTS[style] && STYLE_DEFAULTS[style].color) || '#3b82f6';
|
|
508
959
|
var marker;
|
|
509
|
-
if (style === '
|
|
960
|
+
if (style === 'place') {
|
|
961
|
+
marker = new mapboxgl.Marker({ element: buildPlaceElement(placeIcon(m.maki, m.category), m.color) });
|
|
962
|
+
} else if (style === 'numbered' || style === 'start' || style === 'end') {
|
|
510
963
|
var label = m.label || (style === 'start' ? 'S' : style === 'end' ? 'E' : '');
|
|
511
964
|
marker = new mapboxgl.Marker({ element: buildBadgeElement(label, color) });
|
|
512
965
|
} else {
|
|
513
966
|
marker = new mapboxgl.Marker({ color: color });
|
|
514
967
|
}
|
|
515
968
|
marker.setLngLat(m.coordinates);
|
|
516
|
-
|
|
969
|
+
// A place with an id gets a carousel card instead of a popup: clicking
|
|
970
|
+
// its pin selects the card (see selectPlace).
|
|
971
|
+
if (m.popup && !m.id) marker.setPopup(new mapboxgl.Popup().setText(m.popup));
|
|
517
972
|
marker.addTo(map);
|
|
518
973
|
trackedMarkers.push(marker);
|
|
519
974
|
// Tagged by buildSearchMiniPayload with the result's mapbox_id so the
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
if (m.id)
|
|
975
|
+
// carousel and Place Details enrichment can look the marker back up
|
|
976
|
+
// later without walking trackedMarkers.
|
|
977
|
+
if (m.id) {
|
|
978
|
+
var badgeEl = typeof marker.getElement === 'function' ? marker.getElement() : null;
|
|
979
|
+
markersById[m.id] = { marker: marker, el: badgeEl, color: color };
|
|
980
|
+
if (badgeEl && m.name) {
|
|
981
|
+
var nameEl = document.createElement('span');
|
|
982
|
+
nameEl.className = 'marker-name';
|
|
983
|
+
nameEl.textContent = m.name;
|
|
984
|
+
badgeEl.appendChild(nameEl);
|
|
985
|
+
}
|
|
986
|
+
if (badgeEl && typeof badgeEl.addEventListener === 'function') {
|
|
987
|
+
var placeId = m.id;
|
|
988
|
+
badgeEl.addEventListener('click', function(e) {
|
|
989
|
+
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
|
990
|
+
selectPlace(placeId, { scrollCard: true, panTo: true });
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
}
|
|
523
994
|
bbox.extend(m.coordinates);
|
|
524
995
|
}
|
|
525
996
|
|
|
526
|
-
// Guarantees a marker that opts into the
|
|
527
|
-
// a visible number on the map matching its
|
|
997
|
+
// Guarantees a marker that opts into the carousel (has .id) also gets
|
|
998
|
+
// a visible number on the map matching its card, unless the caller
|
|
528
999
|
// already gave it an explicit label. Without this, a caller that builds
|
|
529
1000
|
// plain unlabeled pins for POI markers (observed live: Claude did this
|
|
530
1001
|
// for inline markers, leaving the map showing generic blue pins with no
|
|
531
|
-
// way to tell which
|
|
532
|
-
//
|
|
1002
|
+
// way to tell which card is which) breaks the whole point of the
|
|
1003
|
+
// numbering. Mutates markers in place, so it must run before
|
|
533
1004
|
// addOneMarker draws them.
|
|
534
1005
|
//
|
|
535
1006
|
// Numbering runs across the whole render, not per call: a render can
|
|
536
1007
|
// draw inline markers and then merge in self-fetched search results, and
|
|
537
1008
|
// each set arrives numbered from 1. The first set keeps the caller's
|
|
538
1009
|
// numbers when they're unique (a visit order, say); a later set, or one
|
|
539
|
-
// with repeated numbers, is renumbered to continue the
|
|
540
|
-
// Letter labels ("A") are kept as they are. A place already in the
|
|
1010
|
+
// with repeated numbers, is renumbered to continue the carousel's count.
|
|
1011
|
+
// Letter labels ("A") are kept as they are. A place already in the carousel
|
|
541
1012
|
// this render is dropped rather than drawn and listed twice. Returns the
|
|
542
1013
|
// markers to draw.
|
|
543
1014
|
function applyPanelNumbering(markers) {
|
|
@@ -564,12 +1035,17 @@ ${initialDataScript}
|
|
|
564
1035
|
fresh.forEach(function(m) {
|
|
565
1036
|
if (!m || !m.id) return;
|
|
566
1037
|
if (m.label && (keep || !isWholeNumber(m.label))) return;
|
|
1038
|
+
// A place with no number of its own gets an icon pin, not a number:
|
|
1039
|
+
// the selected pin and card light up together, which is enough to
|
|
1040
|
+
// tell them apart.
|
|
1041
|
+
if (!m.label && m.style !== 'start' && m.style !== 'end') {
|
|
1042
|
+
m.style = 'place';
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
// Otherwise a numbered pin, or a start/end pin, takes the next number.
|
|
567
1046
|
panelCount += 1;
|
|
568
1047
|
var n = String(panelCount);
|
|
569
|
-
if (
|
|
570
|
-
if (m.style !== 'start' && m.style !== 'end') m.style = 'numbered';
|
|
571
|
-
if (!m.color) m.color = '#f97316';
|
|
572
|
-
} else if (typeof m.popup === 'string' && m.popup.indexOf(m.label + '. ') === 0) {
|
|
1048
|
+
if (m.label && typeof m.popup === 'string' && m.popup.indexOf(m.label + '. ') === 0) {
|
|
573
1049
|
// Search popups start with the old number ("3. Name").
|
|
574
1050
|
m.popup = n + '. ' + m.popup.slice(String(m.label).length + 2);
|
|
575
1051
|
}
|
|
@@ -584,26 +1060,33 @@ ${initialDataScript}
|
|
|
584
1060
|
return n > 0 && String(n) === String(label);
|
|
585
1061
|
}
|
|
586
1062
|
|
|
587
|
-
// Bottom padding that keeps fitted data out from under the
|
|
588
|
-
//
|
|
1063
|
+
// Bottom padding that keeps fitted data out from under the carousel and
|
|
1064
|
+
// the legends above it, unless the map is too short to spare the room.
|
|
589
1065
|
function bottomPadding() {
|
|
590
|
-
|
|
591
|
-
|
|
1066
|
+
var bottom = 0;
|
|
1067
|
+
if (carouselEl.style.display === 'block') bottom = carouselTop() + 12;
|
|
1068
|
+
if (legendEl.style.display === 'block' ||
|
|
1069
|
+
trafficLegendEl.style.display === 'flex') {
|
|
1070
|
+
bottom = Math.max(bottom, legendTop() + 12);
|
|
1071
|
+
}
|
|
592
1072
|
var el = document.getElementById('map');
|
|
593
1073
|
var height = (el && el.clientHeight) || window.innerHeight || 0;
|
|
594
|
-
if (bottom <= 50 || (height && height - bottom - 70 <
|
|
1074
|
+
if (bottom <= 50 || (height && height - bottom - 70 < 120)) return 50;
|
|
595
1075
|
return bottom;
|
|
596
1076
|
}
|
|
597
1077
|
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
function
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
1078
|
+
// Distance from the bottom of the map to the top of the carousel. 36px
|
|
1079
|
+
// keeps it clear of the Mapbox logo and attribution.
|
|
1080
|
+
var CAROUSEL_BOTTOM = 36;
|
|
1081
|
+
function carouselTop() {
|
|
1082
|
+
return CAROUSEL_BOTTOM + (carouselEl.offsetHeight || 100);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// The legends stack sits 40px up (above the Mapbox logo) until the
|
|
1086
|
+
// carousel pushes it higher.
|
|
1087
|
+
function legendTop() {
|
|
1088
|
+
var bottom = parseInt(legendsEl.style.bottom, 10) || 40;
|
|
1089
|
+
return bottom + (legendsEl.offsetHeight || 0);
|
|
607
1090
|
}
|
|
608
1091
|
|
|
609
1092
|
function mapContainerWidth() {
|
|
@@ -617,7 +1100,7 @@ ${initialDataScript}
|
|
|
617
1100
|
setTimeout(function() {
|
|
618
1101
|
map.resize();
|
|
619
1102
|
map.fitBounds(bounds, {
|
|
620
|
-
padding: { top: 70, bottom: bottomPadding(), left: 30, right:
|
|
1103
|
+
padding: { top: 70, bottom: bottomPadding(), left: 30, right: 30 },
|
|
621
1104
|
duration: 600
|
|
622
1105
|
});
|
|
623
1106
|
}, 60);
|
|
@@ -638,14 +1121,16 @@ ${initialDataScript}
|
|
|
638
1121
|
if (payload.summary && summaryEl.style.display === 'none') {
|
|
639
1122
|
summaryEl.textContent = payload.summary;
|
|
640
1123
|
summaryEl.style.display = 'block';
|
|
1124
|
+
placeTopLeftControls();
|
|
641
1125
|
}
|
|
642
|
-
maybeShowSidePanel(markers
|
|
1126
|
+
maybeShowSidePanel(markers);
|
|
643
1127
|
var bounds = bbox.bounds();
|
|
644
1128
|
if (bounds) fitToBounds(bounds);
|
|
645
1129
|
}
|
|
646
1130
|
|
|
647
1131
|
function applyBaseMapConfig(baseMapConfig) {
|
|
648
1132
|
if (!baseMapConfig || typeof baseMapConfig !== 'object') return;
|
|
1133
|
+
currentBaseMapConfig = baseMapConfig;
|
|
649
1134
|
if (typeof map.setConfigProperty !== 'function') return;
|
|
650
1135
|
Object.keys(baseMapConfig).forEach(function(key) {
|
|
651
1136
|
try { map.setConfigProperty('basemap', key, baseMapConfig[key]); } catch (_) { /* not a Standard-based style */ }
|
|
@@ -664,9 +1149,7 @@ ${initialDataScript}
|
|
|
664
1149
|
? 'standard-satellite'
|
|
665
1150
|
: (payload.baseStyle === 'standard' ? 'standard' : null);
|
|
666
1151
|
if (desiredBaseStyleId && desiredBaseStyleId !== currentBaseStyleId) {
|
|
667
|
-
|
|
668
|
-
map.setStyle('mapbox://styles/mapbox/' + desiredBaseStyleId);
|
|
669
|
-
map.once('style.load', function() { renderBody(payload); });
|
|
1152
|
+
switchBaseStyle(desiredBaseStyleId, function() { renderBody(payload); });
|
|
670
1153
|
return;
|
|
671
1154
|
}
|
|
672
1155
|
renderBody(payload);
|
|
@@ -684,21 +1167,14 @@ ${initialDataScript}
|
|
|
684
1167
|
// Covers both inline markers (a caller-composed payload) and self-fetch
|
|
685
1168
|
// markers (merged in later via mergeAdditionalPayload) — see
|
|
686
1169
|
// maybeShowSidePanel below. Whichever path produced id-bearing POI
|
|
687
|
-
// markers gets the same results
|
|
688
|
-
maybeShowSidePanel(markers
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
escapeAttr(row.color) + ';opacity:' + op + '"></span>' +
|
|
696
|
-
escapeText(row.label) + '</div>';
|
|
697
|
-
}).join('');
|
|
698
|
-
legendEl.style.display = 'block';
|
|
699
|
-
} else {
|
|
700
|
-
legendEl.style.display = 'none';
|
|
701
|
-
}
|
|
1170
|
+
// markers gets the same results carousel.
|
|
1171
|
+
maybeShowSidePanel(markers);
|
|
1172
|
+
|
|
1173
|
+
payloadLegend = Array.isArray(payload.legend) ? payload.legend : [];
|
|
1174
|
+
// showTraffic set by the caller wins; left out, keep whatever the user
|
|
1175
|
+
// picked with the Traffic button (a style switch dropped the layer, so
|
|
1176
|
+
// setTraffic puts it back).
|
|
1177
|
+
setTraffic(typeof payload.showTraffic === 'boolean' ? payload.showTraffic : trafficOn);
|
|
702
1178
|
|
|
703
1179
|
loadingEl.style.display = 'none';
|
|
704
1180
|
requestSizeToFit();
|
|
@@ -730,6 +1206,9 @@ ${initialDataScript}
|
|
|
730
1206
|
if (!data) return;
|
|
731
1207
|
var src = map.getSource(payload.defer.layerId);
|
|
732
1208
|
if (src && typeof src.setData === 'function') src.setData(data);
|
|
1209
|
+
drawnLayers.forEach(function(layer) {
|
|
1210
|
+
if (layer.id === payload.defer.layerId) layer.data = data;
|
|
1211
|
+
});
|
|
733
1212
|
},
|
|
734
1213
|
function() { /* ignore — keep what we rendered inline */ }
|
|
735
1214
|
);
|
|
@@ -1389,7 +1868,7 @@ ${initialDataScript}
|
|
|
1389
1868
|
var features = [];
|
|
1390
1869
|
points.forEach(function(f, i) {
|
|
1391
1870
|
var props = f.properties || {};
|
|
1392
|
-
var popupParts = [
|
|
1871
|
+
var popupParts = [props.name || 'Result'];
|
|
1393
1872
|
var addr = props.full_address || props.place_formatted;
|
|
1394
1873
|
if (addr) popupParts.push(addr);
|
|
1395
1874
|
if (typeof props.distance === 'number') {
|
|
@@ -1398,16 +1877,15 @@ ${initialDataScript}
|
|
|
1398
1877
|
var coords = f.geometry.coordinates;
|
|
1399
1878
|
// id/name/category/distanceMeters are the same generic POI fields an
|
|
1400
1879
|
// inline (hand-composed) marker can carry — see maybeShowSidePanel,
|
|
1401
|
-
// which builds the results
|
|
1402
|
-
// mapbox_id is missing on the rare result that lacks one; the
|
|
1880
|
+
// which builds the results carousel from either source identically.
|
|
1881
|
+
// mapbox_id is missing on the rare result that lacks one; the carousel
|
|
1403
1882
|
// just skips that row rather than show an unclickable/unenrichable
|
|
1404
1883
|
// entry.
|
|
1405
1884
|
markers.push({
|
|
1406
1885
|
id: props.mapbox_id,
|
|
1407
1886
|
coordinates: coords,
|
|
1408
|
-
style: '
|
|
1409
|
-
|
|
1410
|
-
color: '#f97316',
|
|
1887
|
+
style: 'place',
|
|
1888
|
+
maki: props.maki,
|
|
1411
1889
|
popup: popupParts.join(' — '),
|
|
1412
1890
|
name: props.name || ('Result ' + (i + 1)),
|
|
1413
1891
|
category: Array.isArray(props.poi_category) ? props.poi_category[0] : undefined,
|
|
@@ -1431,11 +1909,11 @@ ${initialDataScript}
|
|
|
1431
1909
|
data: { type: 'FeatureCollection', features: features },
|
|
1432
1910
|
paint: {
|
|
1433
1911
|
'circle-radius': 6,
|
|
1434
|
-
'circle-color': '#
|
|
1435
|
-
'circle-opacity': 0.
|
|
1912
|
+
'circle-color': '#0f172a',
|
|
1913
|
+
'circle-opacity': 0.1,
|
|
1436
1914
|
'circle-stroke-width': 1,
|
|
1437
|
-
'circle-stroke-color': '#
|
|
1438
|
-
'circle-stroke-opacity': 0.
|
|
1915
|
+
'circle-stroke-color': '#0f172a',
|
|
1916
|
+
'circle-stroke-opacity': 0.25
|
|
1439
1917
|
}
|
|
1440
1918
|
}
|
|
1441
1919
|
] : [];
|
|
@@ -1443,27 +1921,27 @@ ${initialDataScript}
|
|
|
1443
1921
|
return { summary: summary, layers: layers, markers: markers };
|
|
1444
1922
|
}
|
|
1445
1923
|
|
|
1446
|
-
// ---
|
|
1924
|
+
// --- Results carousel: one card per place + Place Details enrichment ---
|
|
1447
1925
|
// Triggered generically by maybeShowSidePanel for ANY marker set that
|
|
1448
1926
|
// includes id-bearing entries — self-fetch search/category-search
|
|
1449
1927
|
// results (via mergeAdditionalPayload) and hand-composed inline markers
|
|
1450
1928
|
// from render_map_tool's own "markers" input (via renderBody) both funnel
|
|
1451
1929
|
// through the same path. Renders the initial (Search Box-only, or
|
|
1452
|
-
// whatever the caller already provided)
|
|
1453
|
-
// never blocks on the slower enrichment call.
|
|
1454
|
-
// shown as the panel heading (e.g. the query, category, or the payload's
|
|
1455
|
-
// own summary).
|
|
1930
|
+
// whatever the caller already provided) cards immediately so the
|
|
1931
|
+
// carousel never blocks on the slower enrichment call.
|
|
1456
1932
|
function derivePanelItems(markers) {
|
|
1457
1933
|
if (!Array.isArray(markers)) return [];
|
|
1458
1934
|
var items = [];
|
|
1459
1935
|
markers.forEach(function(m) {
|
|
1460
1936
|
if (!m || !m.id) return;
|
|
1461
|
-
// The pin's own label, so a
|
|
1937
|
+
// The pin's own label, so a card reads the same as its pin ("A" too).
|
|
1938
|
+
// A place pin has none, and neither does its card.
|
|
1462
1939
|
var badge = isWholeNumber(m.label) ? parseInt(m.label, 10)
|
|
1463
|
-
: (m.label ? String(m.label) :
|
|
1940
|
+
: (m.label ? String(m.label) : null);
|
|
1464
1941
|
items.push({
|
|
1465
1942
|
id: m.id,
|
|
1466
1943
|
number: badge,
|
|
1944
|
+
icon: placeIcon(m.maki, m.category),
|
|
1467
1945
|
name: m.name || ('Result ' + (items.length + 1)),
|
|
1468
1946
|
category: m.category,
|
|
1469
1947
|
distanceMeters: typeof m.distanceMeters === 'number' ? m.distanceMeters : undefined,
|
|
@@ -1475,16 +1953,16 @@ ${initialDataScript}
|
|
|
1475
1953
|
// Exposed so tests can exercise the pure derivation logic without a DOM.
|
|
1476
1954
|
window.__derivePanelItems = derivePanelItems;
|
|
1477
1955
|
|
|
1478
|
-
// Adds
|
|
1479
|
-
// those. A response is applied only if no newer render has started
|
|
1480
|
-
// (panelGeneration), so a slow lookup can't write into
|
|
1481
|
-
function maybeShowSidePanel(markers
|
|
1956
|
+
// Adds cards for places not yet in this render's carousel and enriches
|
|
1957
|
+
// just those. A response is applied only if no newer render has started
|
|
1958
|
+
// since (panelGeneration), so a slow lookup can't write into newer cards.
|
|
1959
|
+
function maybeShowSidePanel(markers) {
|
|
1482
1960
|
var items = derivePanelItems(markers).filter(function(it) {
|
|
1483
1961
|
return !panelIds[it.id];
|
|
1484
1962
|
});
|
|
1485
1963
|
if (items.length === 0) return;
|
|
1486
1964
|
items.forEach(function(it) { panelIds[it.id] = true; });
|
|
1487
|
-
|
|
1965
|
+
appendCards(items);
|
|
1488
1966
|
var generation = panelGeneration;
|
|
1489
1967
|
fetchPlaceDetailsBatch(
|
|
1490
1968
|
items.map(function(it) { return it.id; }),
|
|
@@ -1495,109 +1973,334 @@ ${initialDataScript}
|
|
|
1495
1973
|
});
|
|
1496
1974
|
}
|
|
1497
1975
|
|
|
1498
|
-
// Keyed by mapbox_id -> { li, meta } elements, so enrichSidePanel
|
|
1499
|
-
// update a
|
|
1500
|
-
//
|
|
1976
|
+
// Keyed by mapbox_id -> { li, meta, body } elements, so enrichSidePanel
|
|
1977
|
+
// can update a card directly once Place Details resolves, without
|
|
1978
|
+
// re-parsing markup back into a queryable DOM (innerHTML + querySelector
|
|
1501
1979
|
// round-trips don't work in every MCP Apps host's iframe sandbox, and
|
|
1502
1980
|
// building real nodes up front is simpler to reason about either way).
|
|
1503
|
-
// Reset by clearPanel, same lifecycle as the
|
|
1981
|
+
// Reset by clearPanel, same lifecycle as the carousel itself.
|
|
1504
1982
|
var panelRowsById = {};
|
|
1505
1983
|
// Bumped by teardown(), so responses from an earlier render are dropped.
|
|
1506
1984
|
var panelGeneration = 0;
|
|
1507
1985
|
// Id-bearing markers numbered so far this render, and the ids listed.
|
|
1508
1986
|
var panelCount = 0;
|
|
1509
1987
|
var panelIds = {};
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
var
|
|
1513
|
-
|
|
1988
|
+
var cardListEl = null;
|
|
1989
|
+
var cardOrder = [];
|
|
1990
|
+
var activePlaceId = null;
|
|
1991
|
+
|
|
1992
|
+
// classList when there is one: GL JS adds its own classes to a marker's
|
|
1993
|
+
// element, which assigning className would wipe out.
|
|
1994
|
+
function setActiveClass(el, on) {
|
|
1995
|
+
if (!el) return;
|
|
1996
|
+
if (el.classList && typeof el.classList.toggle === 'function') {
|
|
1997
|
+
el.classList.toggle('active', on);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
var names = String(el.className || '').split(' ').filter(function(c) {
|
|
2001
|
+
return c && c !== 'active';
|
|
2002
|
+
});
|
|
2003
|
+
if (on) names.push('active');
|
|
2004
|
+
el.className = names.join(' ');
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Highlights one place: its card gets a border and its pin inverts and
|
|
2008
|
+
// shows the name. scrollCard brings the card into view (a pin was
|
|
2009
|
+
// clicked); panTo moves the map when the pin is hidden (a card was
|
|
2010
|
+
// picked).
|
|
2011
|
+
function selectPlace(id, opts) {
|
|
2012
|
+
var row = panelRowsById[id];
|
|
2013
|
+
if (!row) return;
|
|
2014
|
+
opts = opts || {};
|
|
2015
|
+
if (activePlaceId !== id) {
|
|
2016
|
+
if (activePlaceId) {
|
|
2017
|
+
var prevRow = panelRowsById[activePlaceId];
|
|
2018
|
+
var prevPin = markersById[activePlaceId];
|
|
2019
|
+
if (prevRow) setActiveClass(prevRow.li, false);
|
|
2020
|
+
if (prevPin && prevPin.el) {
|
|
2021
|
+
setActiveClass(prevPin.el, false);
|
|
2022
|
+
prevPin.el.style.zIndex = '';
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
activePlaceId = id;
|
|
2026
|
+
setActiveClass(row.li, true);
|
|
2027
|
+
var pin = markersById[id];
|
|
2028
|
+
if (pin && pin.el) {
|
|
2029
|
+
setActiveClass(pin.el, true);
|
|
2030
|
+
pin.el.style.zIndex = '2';
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
if (opts.scrollCard && typeof row.li.scrollIntoView === 'function') {
|
|
2034
|
+
row.li.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
2035
|
+
}
|
|
2036
|
+
if (opts.panTo) panToIfHidden(id);
|
|
2037
|
+
}
|
|
2038
|
+
window.__selectPlace = selectPlace;
|
|
1514
2039
|
|
|
1515
|
-
|
|
2040
|
+
// Moves the map only when the pin, or the name label under it, is off
|
|
2041
|
+
// screen or under the carousel, so swiping through nearby results
|
|
2042
|
+
// doesn't keep shifting it.
|
|
2043
|
+
var PIN_LABEL_ROOM = 52;
|
|
2044
|
+
function panToIfHidden(id) {
|
|
2045
|
+
var entry = markersById[id];
|
|
2046
|
+
if (!entry || !map || typeof map.project !== 'function') return;
|
|
2047
|
+
var lngLat = entry.marker.getLngLat();
|
|
2048
|
+
var p = map.project(lngLat);
|
|
2049
|
+
var el = document.getElementById('map');
|
|
2050
|
+
var width = (el && el.clientWidth) || 0;
|
|
2051
|
+
var height = (el && el.clientHeight) || 0;
|
|
2052
|
+
if (!width || !height) return;
|
|
2053
|
+
var visibleBottom = height - carouselTop() - PIN_LABEL_ROOM;
|
|
2054
|
+
if (p.x >= 24 && p.x <= width - 24 && p.y >= 24 && p.y <= visibleBottom) return;
|
|
2055
|
+
// Just far enough to bring it 24px inside the visible area, so the
|
|
2056
|
+
// pins around it mostly stay in view.
|
|
2057
|
+
var x = Math.min(Math.max(p.x, 48), width - 48);
|
|
2058
|
+
var y = Math.min(Math.max(p.y, 48), Math.max(48, visibleBottom - 24));
|
|
2059
|
+
map.panBy([p.x - x, p.y - y], { duration: 500 });
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
function buildCard(it) {
|
|
1516
2063
|
var li = document.createElement('li');
|
|
1517
|
-
li.className = '
|
|
2064
|
+
li.className = 'card';
|
|
1518
2065
|
|
|
1519
|
-
var badge =
|
|
1520
|
-
|
|
1521
|
-
|
|
2066
|
+
var badge = null;
|
|
2067
|
+
if (it.number !== null && it.number !== undefined) {
|
|
2068
|
+
badge = document.createElement('span');
|
|
2069
|
+
badge.className = 'card-badge';
|
|
2070
|
+
badge.textContent = String(it.number);
|
|
2071
|
+
}
|
|
1522
2072
|
// Match the pin when the caller colored it (e.g. gray for "other
|
|
1523
2073
|
// results" next to a legend); the default orange is in the CSS.
|
|
1524
|
-
if (typeof it.color === 'string' && it.color.toLowerCase() !== '#f97316' &&
|
|
2074
|
+
if (badge && typeof it.color === 'string' && it.color.toLowerCase() !== '#f97316' &&
|
|
1525
2075
|
(/^#[0-9a-f]{3,8}$/i).test(it.color)) {
|
|
1526
2076
|
badge.style.background = it.color;
|
|
1527
|
-
badge.style.color =
|
|
2077
|
+
badge.style.color = badgeTextColor(it.color);
|
|
1528
2078
|
}
|
|
1529
2079
|
|
|
1530
2080
|
var name = document.createElement('div');
|
|
1531
|
-
name.className = '
|
|
2081
|
+
name.className = 'card-name';
|
|
1532
2082
|
name.textContent = it.name;
|
|
1533
2083
|
|
|
1534
|
-
var metaParts = [];
|
|
1535
|
-
if (it.category) metaParts.push(it.category);
|
|
1536
|
-
if (typeof it.distanceMeters === 'number') {
|
|
1537
|
-
metaParts.push(Math.round(it.distanceMeters) + ' m');
|
|
1538
|
-
}
|
|
1539
2084
|
var meta = document.createElement('div');
|
|
1540
|
-
meta.className = '
|
|
1541
|
-
meta.textContent =
|
|
2085
|
+
meta.className = 'card-meta';
|
|
2086
|
+
meta.textContent = cardMetaText(it, null);
|
|
1542
2087
|
|
|
1543
2088
|
var body = document.createElement('div');
|
|
2089
|
+
body.className = 'card-body';
|
|
1544
2090
|
body.appendChild(name);
|
|
1545
2091
|
body.appendChild(meta);
|
|
1546
|
-
|
|
2092
|
+
// Placeholder picture, replaced by the photo if one loads (see
|
|
2093
|
+
// loadPanelPhoto). Shown first by CSS order.
|
|
2094
|
+
var thumb = document.createElement('span');
|
|
2095
|
+
thumb.className = 'card-thumb empty';
|
|
2096
|
+
thumb.innerHTML = iconSvg(it.icon);
|
|
2097
|
+
if (typeof thumb.setAttribute === 'function') {
|
|
2098
|
+
thumb.setAttribute('role', 'img');
|
|
2099
|
+
thumb.setAttribute('aria-label', it.name);
|
|
2100
|
+
}
|
|
2101
|
+
if (badge) li.appendChild(badge);
|
|
1547
2102
|
li.appendChild(body);
|
|
2103
|
+
li.appendChild(thumb);
|
|
1548
2104
|
|
|
1549
2105
|
li.addEventListener('click', function() {
|
|
1550
|
-
|
|
1551
|
-
if (entry) map.flyTo({ center: entry.marker.getLngLat(), zoom: 15, duration: 500 });
|
|
2106
|
+
selectPlace(it.id, { scrollCard: true, panTo: true });
|
|
1552
2107
|
});
|
|
1553
2108
|
|
|
1554
|
-
panelRowsById[it.id] = { li: li, meta: meta };
|
|
2109
|
+
panelRowsById[it.id] = { li: li, meta: meta, body: body, thumb: thumb, item: it };
|
|
1555
2110
|
return li;
|
|
1556
2111
|
}
|
|
1557
2112
|
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2113
|
+
// "Cafe · $$ · 120 m". Search Box categories come lowercase.
|
|
2114
|
+
function cardMetaText(it, price) {
|
|
2115
|
+
var parts = [];
|
|
2116
|
+
if (it.category) parts.push(it.category.charAt(0).toUpperCase() + it.category.slice(1));
|
|
2117
|
+
if (price) parts.push(price);
|
|
2118
|
+
if (typeof it.distanceMeters === 'number') {
|
|
2119
|
+
parts.push(Math.round(it.distanceMeters) + ' m');
|
|
2120
|
+
}
|
|
2121
|
+
return parts.join(' · ');
|
|
1562
2122
|
}
|
|
1563
2123
|
|
|
1564
|
-
|
|
1565
|
-
|
|
2124
|
+
// Places API attributes.price_level as dollar signs. Unknown levels show
|
|
2125
|
+
// nothing rather than a guess.
|
|
2126
|
+
var PRICE_SIGNS = { 'cheap': '$', 'moderate': '$$', 'expensive': '$$$', 'very expensive': '$$$$' };
|
|
2127
|
+
function priceSigns(level) {
|
|
2128
|
+
if (typeof level !== 'string') return null;
|
|
2129
|
+
return PRICE_SIGNS[level.trim().toLowerCase()] || null;
|
|
1566
2130
|
}
|
|
2131
|
+
window.__priceSigns = priceSigns;
|
|
1567
2132
|
|
|
1568
|
-
function
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
2133
|
+
function clearPanel() {
|
|
2134
|
+
while (carouselEl.firstChild) carouselEl.removeChild(carouselEl.firstChild);
|
|
2135
|
+
panelRowsById = {};
|
|
2136
|
+
cardListEl = null;
|
|
2137
|
+
cardOrder = [];
|
|
2138
|
+
activePlaceId = null;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// Swiping selects whichever card ends up in the middle.
|
|
2142
|
+
var scrollTimer = null;
|
|
2143
|
+
function onCardScroll() {
|
|
2144
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
2145
|
+
scrollTimer = setTimeout(function() {
|
|
2146
|
+
scrollTimer = null;
|
|
2147
|
+
if (!cardListEl) return;
|
|
2148
|
+
var center = (cardListEl.scrollLeft || 0) + (cardListEl.clientWidth || 0) / 2;
|
|
2149
|
+
var best = null;
|
|
2150
|
+
var bestDist = Infinity;
|
|
2151
|
+
cardOrder.forEach(function(id) {
|
|
2152
|
+
var row = panelRowsById[id];
|
|
2153
|
+
if (!row) return;
|
|
2154
|
+
var mid = (row.li.offsetLeft || 0) + (row.li.offsetWidth || 0) / 2;
|
|
2155
|
+
var d = Math.abs(mid - center);
|
|
2156
|
+
if (d < bestDist) { bestDist = d; best = id; }
|
|
2157
|
+
});
|
|
2158
|
+
if (best && best !== activePlaceId) selectPlace(best, { panTo: true });
|
|
2159
|
+
}, 120);
|
|
1572
2160
|
}
|
|
1573
2161
|
|
|
1574
|
-
// Creates the
|
|
1575
|
-
//
|
|
1576
|
-
// markers), so every numbered pin has a
|
|
1577
|
-
|
|
2162
|
+
// Creates the carousel on the render's first id-bearing markers, and adds
|
|
2163
|
+
// cards for later ones (e.g. search results merged in after inline
|
|
2164
|
+
// markers), so every numbered pin has a card. The first card starts
|
|
2165
|
+
// selected.
|
|
2166
|
+
function appendCards(items) {
|
|
1578
2167
|
if (!Array.isArray(items) || items.length === 0) return;
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2168
|
+
var created = false;
|
|
2169
|
+
if (!cardListEl) {
|
|
2170
|
+
cardListEl = document.createElement('ul');
|
|
2171
|
+
cardListEl.className = 'card-list';
|
|
2172
|
+
cardListEl.addEventListener('scroll', onCardScroll);
|
|
2173
|
+
carouselEl.appendChild(cardListEl);
|
|
2174
|
+
carouselEl.style.display = 'block';
|
|
2175
|
+
created = true;
|
|
2176
|
+
}
|
|
2177
|
+
var list = cardListEl;
|
|
2178
|
+
items.forEach(function(it) {
|
|
2179
|
+
list.appendChild(buildCard(it));
|
|
2180
|
+
cardOrder.push(it.id);
|
|
2181
|
+
});
|
|
2182
|
+
// The legends (bottom-left) move up above the cards.
|
|
2183
|
+
legendsEl.style.bottom = (carouselTop() + 8) + 'px';
|
|
2184
|
+
if (created) selectPlace(items[0].id);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Opening hours from a Places API opening_hours string, which uses OSM
|
|
2188
|
+
// syntax: "Mo-Fr 07:00-19:00; Sa,Su 08:00-17:00", "Mo 09:00-12:00,13:00-17:00",
|
|
2189
|
+
// "Su off", "24/7". Returns null for anything else (holidays, months, week
|
|
2190
|
+
// numbers), so a card shows no hours rather than wrong ones.
|
|
2191
|
+
//
|
|
2192
|
+
// With the place's time zone (Places telemetry.tags.timezone) we know its
|
|
2193
|
+
// clock, so the card says whether it's open now: { state: 'open', text:
|
|
2194
|
+
// 'until 8:30 PM' } or { state: 'closed', text: '· opens 7:30 AM' }.
|
|
2195
|
+
// Without one we only know the viewer's day, so the card shows today's
|
|
2196
|
+
// hours: { state: 'hours', text: 'Open 7:30 AM–3:00 PM' }. Times use the
|
|
2197
|
+
// viewer's locale (7:30–15:00 in en-GB).
|
|
2198
|
+
var OSM_WEEK = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
2199
|
+
var INTL_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
2200
|
+
var HOURS_RANGE = /^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$/;
|
|
2201
|
+
function osmDaysInclude(days, day) {
|
|
2202
|
+
var parts = days.split(',');
|
|
2203
|
+
var hit = false;
|
|
2204
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2205
|
+
var ends = parts[i].split('-');
|
|
2206
|
+
var a = OSM_WEEK.indexOf(ends[0]);
|
|
2207
|
+
var b = ends.length === 2 ? OSM_WEEK.indexOf(ends[1]) : a;
|
|
2208
|
+
if (a === -1 || b === -1 || ends.length > 2) return null;
|
|
2209
|
+
if (a <= b ? (day >= a && day <= b) : (day >= a || day <= b)) hit = true;
|
|
2210
|
+
}
|
|
2211
|
+
return hit;
|
|
2212
|
+
}
|
|
2213
|
+
function toMinutes(hhmm) {
|
|
2214
|
+
return parseInt(hhmm.slice(0, 2), 10) * 60 + parseInt(hhmm.slice(3, 5), 10);
|
|
2215
|
+
}
|
|
2216
|
+
// A day's opening ranges as [startMinute, endMinute] pairs (end <= start
|
|
2217
|
+
// runs past midnight), 'always' for 24/7, or null when unparseable.
|
|
2218
|
+
function osmDayRanges(spec, day) {
|
|
2219
|
+
if (spec === '24/7') return 'always';
|
|
2220
|
+
var result = [];
|
|
2221
|
+
var rules = spec.split(';');
|
|
2222
|
+
for (var i = 0; i < rules.length; i++) {
|
|
2223
|
+
var rule = rules[i].trim();
|
|
2224
|
+
if (!rule) continue;
|
|
2225
|
+
var space = rule.indexOf(' ');
|
|
2226
|
+
if (space === -1) return null;
|
|
2227
|
+
var applies = osmDaysInclude(rule.slice(0, space), day);
|
|
2228
|
+
if (applies === null) return null;
|
|
2229
|
+
var times = rule.slice(space + 1).trim();
|
|
2230
|
+
var ranges = times === 'off' || times === 'closed' ? [] : times.split(',');
|
|
2231
|
+
var parsed = [];
|
|
2232
|
+
for (var j = 0; j < ranges.length; j++) {
|
|
2233
|
+
var r = ranges[j].trim();
|
|
2234
|
+
if (!HOURS_RANGE.test(r)) return null;
|
|
2235
|
+
parsed.push([toMinutes(r.slice(0, 5)), toMinutes(r.slice(6, 11))]);
|
|
2236
|
+
}
|
|
2237
|
+
// In OSM a later rule for the same day overrides an earlier one.
|
|
2238
|
+
if (applies) result = parsed;
|
|
2239
|
+
}
|
|
2240
|
+
return result;
|
|
2241
|
+
}
|
|
2242
|
+
// The place's weekday (0 = Monday) and minute of the day, or null for a
|
|
2243
|
+
// missing or unknown time zone.
|
|
2244
|
+
function clockIn(timeZone, now) {
|
|
2245
|
+
if (typeof timeZone !== 'string' || !timeZone) return null;
|
|
2246
|
+
var parts;
|
|
2247
|
+
try {
|
|
2248
|
+
parts = new Intl.DateTimeFormat('en-US', {
|
|
2249
|
+
timeZone: timeZone, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
|
|
2250
|
+
}).formatToParts(now);
|
|
2251
|
+
} catch (e) {
|
|
2252
|
+
return null;
|
|
2253
|
+
}
|
|
2254
|
+
var p = {};
|
|
2255
|
+
parts.forEach(function(part) { p[part.type] = part.value; });
|
|
2256
|
+
var day = INTL_WEEK.indexOf(p.weekday);
|
|
2257
|
+
if (day === -1) return null;
|
|
2258
|
+
return { day: day, minutes: (parseInt(p.hour, 10) % 24) * 60 + parseInt(p.minute, 10) };
|
|
2259
|
+
}
|
|
2260
|
+
function formatMinutes(minutes, locale) {
|
|
2261
|
+
var d = new Date(2000, 0, 1, Math.floor(minutes / 60) % 24, minutes % 60);
|
|
2262
|
+
try {
|
|
2263
|
+
return new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(d);
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(d);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function hoursToday(spec, now, timeZone, locale) {
|
|
2269
|
+
if (typeof spec !== 'string') return null;
|
|
2270
|
+
var text = spec.trim();
|
|
2271
|
+
if (!text) return null;
|
|
2272
|
+
var clock = clockIn(timeZone, now);
|
|
2273
|
+
var day = clock ? clock.day : (now.getDay() + 6) % 7;
|
|
2274
|
+
var today = osmDayRanges(text, day);
|
|
2275
|
+
if (today === null) return null;
|
|
2276
|
+
if (today === 'always') return { state: 'open', text: '24 hours' };
|
|
2277
|
+
var fmt = function(m) { return formatMinutes(m, locale); };
|
|
2278
|
+
if (!clock) {
|
|
2279
|
+
if (today.length === 0) return { state: 'closed', text: 'today' };
|
|
2280
|
+
return {
|
|
2281
|
+
state: 'hours',
|
|
2282
|
+
text: 'Open ' + today.map(function(r) { return fmt(r[0]) + '–' + fmt(r[1]); }).join(', ')
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
var t = clock.minutes;
|
|
2286
|
+
// Still open from a range that started yesterday and runs past midnight.
|
|
2287
|
+
var yesterday = osmDayRanges(text, (day + 6) % 7) || [];
|
|
2288
|
+
for (var i = 0; i < yesterday.length; i++) {
|
|
2289
|
+
if (yesterday[i][1] <= yesterday[i][0] && t < yesterday[i][1]) {
|
|
2290
|
+
return { state: 'open', text: 'until ' + fmt(yesterday[i][1]) };
|
|
1594
2291
|
}
|
|
1595
|
-
applyPanelCollapsed();
|
|
1596
|
-
panelEl.style.display = 'flex';
|
|
1597
2292
|
}
|
|
1598
|
-
var
|
|
1599
|
-
|
|
2293
|
+
var next = null;
|
|
2294
|
+
for (var j = 0; j < today.length; j++) {
|
|
2295
|
+
var s = today[j][0];
|
|
2296
|
+
var e = today[j][1];
|
|
2297
|
+
var overnight = e <= s;
|
|
2298
|
+
if (t >= s && (overnight || t < e)) return { state: 'open', text: 'until ' + fmt(e) };
|
|
2299
|
+
if (t < s && (next === null || s < next)) next = s;
|
|
2300
|
+
}
|
|
2301
|
+
return { state: 'closed', text: next === null ? '' : '· opens ' + fmt(next) };
|
|
1600
2302
|
}
|
|
2303
|
+
window.__hoursToday = hoursToday;
|
|
1601
2304
|
|
|
1602
2305
|
// Search Box results mix Mapbox-native POI ids (decode to
|
|
1603
2306
|
// "urn:mbxpoi:<uuid>") with OSM-sourced ones (decode to
|
|
@@ -1623,7 +2326,7 @@ ${initialDataScript}
|
|
|
1623
2326
|
|
|
1624
2327
|
// Places API Details endpoint — batch form. Mirrors the single-record
|
|
1625
2328
|
// GET src/tools/place-details-tool/PlaceDetailsTool.ts calls server-side,
|
|
1626
|
-
// but the
|
|
2329
|
+
// but the carousel needs details for every result at once, so this uses the
|
|
1627
2330
|
// POST /details/retrieve batch form (up to 100 ids) instead of one
|
|
1628
2331
|
// request per pin. Docs: https://docs.mapbox.com/api/search/places/
|
|
1629
2332
|
// Like every other self-fetch call, this runs client-side against the
|
|
@@ -1658,18 +2361,18 @@ ${initialDataScript}
|
|
|
1658
2361
|
});
|
|
1659
2362
|
};
|
|
1660
2363
|
return post(compatibleIds.slice(0, 100), false)
|
|
1661
|
-
// Enrichment is a nice-to-have on top of
|
|
2364
|
+
// Enrichment is a nice-to-have on top of already-rendered cards —
|
|
1662
2365
|
// Public Preview quota (1,000 records/month) or a transient error
|
|
1663
2366
|
// here should never surface as a map error, just leave the
|
|
1664
|
-
// Search-Box-only
|
|
2367
|
+
// Search-Box-only cards as they are.
|
|
1665
2368
|
.catch(function() { return null; });
|
|
1666
2369
|
}
|
|
1667
2370
|
|
|
1668
2371
|
// Place Details photos are hosted wherever each business's site hosts
|
|
1669
2372
|
// them, so the in-chat CSP (mapAppCsp.ts) can't allow them. Inside a host,
|
|
1670
2373
|
// the page asks the server for the photo (PlacePhotoResource, by mapbox_id)
|
|
1671
|
-
// and shows it as a data: URL. A
|
|
1672
|
-
//
|
|
2374
|
+
// and shows it as a data: URL. A page with no host has no CSP of ours, so
|
|
2375
|
+
// it loads the photo URL directly.
|
|
1673
2376
|
var MAX_PANEL_PHOTOS = 10;
|
|
1674
2377
|
var EMBEDDED = window.parent !== window;
|
|
1675
2378
|
var PHOTO_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
@@ -1677,17 +2380,16 @@ ${initialDataScript}
|
|
|
1677
2380
|
|
|
1678
2381
|
function loadPanelPhoto(row, id, photoUrl) {
|
|
1679
2382
|
var show = function(src) {
|
|
1680
|
-
// A later render may have replaced the
|
|
2383
|
+
// A later render may have replaced the carousel while this loaded.
|
|
1681
2384
|
if (panelRowsById[id] !== row) return;
|
|
1682
|
-
|
|
1683
|
-
thumb.
|
|
1684
|
-
thumb.style.backgroundImage = 'url("' + src.replace(/"/g, '') + '")';
|
|
1685
|
-
row.li.appendChild(thumb);
|
|
2385
|
+
row.thumb.className = 'card-thumb';
|
|
2386
|
+
row.thumb.innerHTML = '';
|
|
2387
|
+
row.thumb.style.backgroundImage = 'url("' + src.replace(/"/g, '') + '")';
|
|
1686
2388
|
};
|
|
1687
2389
|
if (!EMBEDDED || !hostAnswered) {
|
|
1688
|
-
// No MCP host (a standalone or framed
|
|
1689
|
-
//
|
|
1690
|
-
// would leave an empty box
|
|
2390
|
+
// No MCP host (a standalone or framed page), so no CSP of ours
|
|
2391
|
+
// either. Load it first: some sites refuse hotlinked photos (403),
|
|
2392
|
+
// which would leave an empty box on the card.
|
|
1691
2393
|
if (photoUrl.indexOf('https://') !== 0 || typeof Image !== 'function') return;
|
|
1692
2394
|
var img = new Image();
|
|
1693
2395
|
img.onload = function() { show(photoUrl); };
|
|
@@ -1712,23 +2414,36 @@ ${initialDataScript}
|
|
|
1712
2414
|
detailsResult.results.forEach(function(place) {
|
|
1713
2415
|
if (!place || !place.mapbox_id) return;
|
|
1714
2416
|
var row = panelRowsById[place.mapbox_id];
|
|
1715
|
-
if (row)
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
}
|
|
1721
|
-
if (place.score && typeof place.score.popularity === 'number') {
|
|
1722
|
-
var extra = Math.round(place.score.popularity * 100) + '% popularity';
|
|
1723
|
-
row.meta.textContent = row.meta.textContent ? row.meta.textContent + ' · ' + extra : extra;
|
|
1724
|
-
}
|
|
2417
|
+
if (!row) return;
|
|
2418
|
+
var photoUrl = place.photos && place.photos[0] && place.photos[0].url;
|
|
2419
|
+
if (typeof photoUrl === 'string' && photosRequested < MAX_PANEL_PHOTOS) {
|
|
2420
|
+
photosRequested++;
|
|
2421
|
+
loadPanelPhoto(row, place.mapbox_id, photoUrl);
|
|
1725
2422
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2423
|
+
var price = priceSigns(place.attributes && place.attributes.price_level);
|
|
2424
|
+
if (price) row.meta.textContent = cardMetaText(row.item, price);
|
|
2425
|
+
// Hours on a second line: a green "Open" or red "Closed" when we know
|
|
2426
|
+
// the place's clock. Places has no star rating, and score.popularity
|
|
2427
|
+
// isn't one, so the card shows neither.
|
|
2428
|
+
var timeZone = place.telemetry && place.telemetry.tags && place.telemetry.tags.timezone;
|
|
2429
|
+
var hours = hoursToday(place.opening_hours, new Date(), timeZone);
|
|
2430
|
+
if (hours) {
|
|
2431
|
+
var line = document.createElement('div');
|
|
2432
|
+
line.className = 'card-extra';
|
|
2433
|
+
if (hours.state === 'hours') {
|
|
2434
|
+
line.textContent = hours.text;
|
|
2435
|
+
} else {
|
|
2436
|
+
var status = document.createElement('span');
|
|
2437
|
+
status.className = hours.state === 'open' ? 'hours-open' : 'hours-closed';
|
|
2438
|
+
status.textContent = hours.state === 'open' ? 'Open' : 'Closed';
|
|
2439
|
+
line.appendChild(status);
|
|
2440
|
+
if (hours.text) {
|
|
2441
|
+
var rest = document.createElement('span');
|
|
2442
|
+
rest.textContent = ' ' + hours.text;
|
|
2443
|
+
line.appendChild(rest);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
row.body.appendChild(line);
|
|
1732
2447
|
}
|
|
1733
2448
|
});
|
|
1734
2449
|
}
|
|
@@ -2044,7 +2759,7 @@ ${initialDataScript}
|
|
|
2044
2759
|
|
|
2045
2760
|
// Mirrors the deleted server-side buildGroundLocationPayload (now
|
|
2046
2761
|
// client-only since the map preview self-fetches): a dark origin pin plus
|
|
2047
|
-
//
|
|
2762
|
+
// place pins for the POIs. Isochrone contours aren't drawn here either
|
|
2048
2763
|
// — the original never rendered them, only a contour-minutes summary.
|
|
2049
2764
|
function buildGroundLocationMiniPayload(place, longitude, latitude, pois) {
|
|
2050
2765
|
var markers = [
|
|
@@ -2056,7 +2771,7 @@ ${initialDataScript}
|
|
|
2056
2771
|
}
|
|
2057
2772
|
];
|
|
2058
2773
|
(pois || []).forEach(function(poi, i) {
|
|
2059
|
-
var parts = [
|
|
2774
|
+
var parts = [poi.name];
|
|
2060
2775
|
if (poi.address) parts.push(poi.address);
|
|
2061
2776
|
if (typeof poi.distance_meters === 'number') {
|
|
2062
2777
|
parts.push(Math.round(poi.distance_meters) + ' m');
|
|
@@ -2064,9 +2779,7 @@ ${initialDataScript}
|
|
|
2064
2779
|
markers.push({
|
|
2065
2780
|
id: poi.id,
|
|
2066
2781
|
coordinates: [poi.longitude, poi.latitude],
|
|
2067
|
-
style: '
|
|
2068
|
-
label: String(i + 1),
|
|
2069
|
-
color: '#f97316',
|
|
2782
|
+
style: 'place',
|
|
2070
2783
|
popup: parts.join(' — '),
|
|
2071
2784
|
name: poi.name,
|
|
2072
2785
|
category: poi.category,
|