@mapbox/mcp-server 0.17.0-dev.1 → 0.17.0-dev.3
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 +977 -252
- 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/MapboxApiBasedTool.d.ts.map +1 -1
- package/dist/commonjs/tools/MapboxApiBasedTool.js +6 -2
- package/dist/commonjs/tools/MapboxApiBasedTool.js.map +1 -1
- package/dist/commonjs/tools/category-search-tool/CategorySearchTool.d.ts.map +1 -1
- package/dist/commonjs/tools/category-search-tool/CategorySearchTool.js +1 -1
- package/dist/commonjs/tools/category-search-tool/CategorySearchTool.js.map +1 -1
- package/dist/commonjs/tools/list-public-tokens-tool/ListPublicTokensTool.d.ts.map +1 -1
- package/dist/commonjs/tools/list-public-tokens-tool/ListPublicTokensTool.js +2 -0
- package/dist/commonjs/tools/list-public-tokens-tool/ListPublicTokensTool.js.map +1 -1
- 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/search-and-geocode-tool/SearchAndGeocodeTool.d.ts.map +1 -1
- package/dist/commonjs/tools/search-and-geocode-tool/SearchAndGeocodeTool.js +1 -1
- package/dist/commonjs/tools/search-and-geocode-tool/SearchAndGeocodeTool.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/utils/selfFetchRef.d.ts +24 -0
- package/dist/commonjs/utils/selfFetchRef.d.ts.map +1 -1
- package/dist/commonjs/utils/selfFetchRef.js +68 -0
- package/dist/commonjs/utils/selfFetchRef.js.map +1 -1
- package/dist/commonjs/utils/serverInstructions.js +1 -1
- package/dist/commonjs/utils/serverInstructions.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 +977 -252
- 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/MapboxApiBasedTool.d.ts.map +1 -1
- package/dist/esm/tools/MapboxApiBasedTool.js +6 -2
- package/dist/esm/tools/MapboxApiBasedTool.js.map +1 -1
- package/dist/esm/tools/category-search-tool/CategorySearchTool.d.ts.map +1 -1
- package/dist/esm/tools/category-search-tool/CategorySearchTool.js +2 -2
- package/dist/esm/tools/category-search-tool/CategorySearchTool.js.map +1 -1
- package/dist/esm/tools/list-public-tokens-tool/ListPublicTokensTool.d.ts.map +1 -1
- package/dist/esm/tools/list-public-tokens-tool/ListPublicTokensTool.js +2 -0
- package/dist/esm/tools/list-public-tokens-tool/ListPublicTokensTool.js.map +1 -1
- 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/search-and-geocode-tool/SearchAndGeocodeTool.d.ts.map +1 -1
- package/dist/esm/tools/search-and-geocode-tool/SearchAndGeocodeTool.js +2 -2
- package/dist/esm/tools/search-and-geocode-tool/SearchAndGeocodeTool.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/utils/selfFetchRef.d.ts +24 -0
- package/dist/esm/utils/selfFetchRef.d.ts.map +1 -1
- package/dist/esm/utils/selfFetchRef.js +67 -0
- package/dist/esm/utils/selfFetchRef.js.map +1 -1
- package/dist/esm/utils/serverInstructions.js +1 -1
- package/dist/esm/utils/serverInstructions.js.map +1 -1
- package/dist/esm/version.json +3 -3
- package/package.json +1 -1
|
@@ -35,24 +35,62 @@ function renderMapAppHtml(params) {
|
|
|
35
35
|
<link href="https://api.mapbox.com/mapbox-gl-js/v${glVersion}/mapbox-gl.css" rel="stylesheet">
|
|
36
36
|
<script src="https://api.mapbox.com/mapbox-gl-js/v${glVersion}/mapbox-gl.js"></script>
|
|
37
37
|
<style>
|
|
38
|
+
/* Light and dark, following the host's theme (see applyHostContext), or
|
|
39
|
+
the system's when the host sends none. Colors come from the host's own
|
|
40
|
+
style variables (MCP Apps hostContext.styles) when it sends them, so
|
|
41
|
+
the cards match the chat around them; the fallbacks are used
|
|
42
|
+
otherwise. The map, its pins and their name labels stay as they are. */
|
|
43
|
+
:root {
|
|
44
|
+
color-scheme: light;
|
|
45
|
+
--ui-surface: var(--color-background-primary, #ffffff);
|
|
46
|
+
--ui-surface-muted: var(--color-background-secondary, #f1f5f9);
|
|
47
|
+
--ui-text: var(--color-text-primary, #0f172a);
|
|
48
|
+
--ui-text-muted: var(--color-text-secondary, #475569);
|
|
49
|
+
--ui-icon-muted: #64748b;
|
|
50
|
+
--ui-shadow: 0 2px 10px rgba(15, 23, 42, 0.18);
|
|
51
|
+
--ui-error-bg: #ffebee; --ui-error-text: #b71c1c;
|
|
52
|
+
}
|
|
53
|
+
:root[data-theme="dark"] {
|
|
54
|
+
color-scheme: dark;
|
|
55
|
+
--ui-surface: var(--color-background-primary, #0f172a);
|
|
56
|
+
--ui-surface-muted: var(--color-background-secondary, #1e293b);
|
|
57
|
+
--ui-text: var(--color-text-primary, #f1f5f9);
|
|
58
|
+
--ui-text-muted: var(--color-text-secondary, #cbd5e1);
|
|
59
|
+
--ui-icon-muted: #94a3b8;
|
|
60
|
+
--ui-shadow: 0 2px 10px rgba(0, 0, 0, 0.45);
|
|
61
|
+
--ui-error-bg: #3b1214; --ui-error-text: #fca5a5;
|
|
62
|
+
}
|
|
38
63
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
39
64
|
html, body { width: 100%; height: 100%; overflow: hidden;
|
|
40
65
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
41
66
|
#map { position: absolute; inset: 0; }
|
|
42
67
|
#summary {
|
|
43
68
|
position: absolute; top: 12px; left: 12px; z-index: 10;
|
|
44
|
-
background:
|
|
69
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
45
70
|
padding: 8px 12px; border-radius: 6px;
|
|
46
71
|
font-size: 13px; font-weight: 500;
|
|
47
|
-
box-shadow:
|
|
72
|
+
box-shadow: var(--ui-shadow);
|
|
48
73
|
max-width: calc(100% - 24px); white-space: pre-line;
|
|
49
74
|
}
|
|
75
|
+
/* Bottom offsets keep the Mapbox logo (bottom left) and attribution
|
|
76
|
+
(bottom right) visible, as the Mapbox terms require. */
|
|
77
|
+
#legends {
|
|
78
|
+
position: absolute; bottom: 40px; left: 12px; z-index: 10;
|
|
79
|
+
display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
|
|
80
|
+
}
|
|
50
81
|
#legend {
|
|
51
|
-
|
|
52
|
-
background: rgba(15, 23, 42, 0.88); color: #f1f5f9;
|
|
82
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
53
83
|
padding: 8px 12px; border-radius: 6px;
|
|
54
|
-
font-size: 11px;
|
|
84
|
+
font-size: 11px; box-shadow: var(--ui-shadow);
|
|
85
|
+
}
|
|
86
|
+
/* Traffic: Fast [4 colors] Slow, below any payload legend. */
|
|
87
|
+
#traffic-legend {
|
|
88
|
+
align-items: center; gap: 8px; padding: 8px; border-radius: 8px;
|
|
89
|
+
background: #fff; color: #05070a; font-size: 14px; line-height: 1.3;
|
|
90
|
+
box-shadow: 0 4px 8px rgba(0,0,0,0.12); white-space: nowrap;
|
|
55
91
|
}
|
|
92
|
+
#traffic-legend .bar { display: flex; gap: 2px; }
|
|
93
|
+
#traffic-legend .bar span { width: 20px; height: 10px; border-radius: 2px; }
|
|
56
94
|
#legend .row { display: flex; align-items: center; margin: 2px 0; }
|
|
57
95
|
#legend .swatch {
|
|
58
96
|
display: inline-block; width: 14px; height: 14px;
|
|
@@ -60,11 +98,11 @@ function renderMapAppHtml(params) {
|
|
|
60
98
|
}
|
|
61
99
|
#loading {
|
|
62
100
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
63
|
-
color:
|
|
101
|
+
color: var(--ui-text-muted); font-size: 16px; z-index: 10; pointer-events: none;
|
|
64
102
|
}
|
|
65
103
|
#error {
|
|
66
104
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
67
|
-
color:
|
|
105
|
+
color: var(--ui-error-text); background: var(--ui-error-bg); border-radius: 8px;
|
|
68
106
|
padding: 20px; max-width: 400px; text-align: center; z-index: 10;
|
|
69
107
|
}
|
|
70
108
|
.marker-badge {
|
|
@@ -75,51 +113,99 @@ function renderMapAppHtml(params) {
|
|
|
75
113
|
box-shadow: 0 2px 6px rgba(0,0,0,0.4);
|
|
76
114
|
cursor: pointer;
|
|
77
115
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
background:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
/*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
116
|
+
/* Selected place (see selectPlace): the pin inverts, like the card it
|
|
117
|
+
matches. */
|
|
118
|
+
.marker-badge.active {
|
|
119
|
+
background: #fff !important; color: #0f172a !important;
|
|
120
|
+
transform: scale(1.18); box-shadow: 0 3px 10px rgba(0,0,0,0.5);
|
|
121
|
+
}
|
|
122
|
+
/* Satellite / Traffic buttons under the zoom control (see buildLayerToggles). */
|
|
123
|
+
.mapboxgl-ctrl-group.layer-toggles button {
|
|
124
|
+
display: flex; align-items: center; justify-content: center; color: #333;
|
|
125
|
+
}
|
|
126
|
+
.mapboxgl-ctrl-group.layer-toggles button.active { color: #2563eb; background: #dbeafe; }
|
|
127
|
+
.mapboxgl-ctrl-group.layer-toggles button:focus:not(:focus-visible) { box-shadow: none; }
|
|
128
|
+
/* Every carousel place shows its name under the pin, with a white halo
|
|
129
|
+
instead of a box, the way map labels look. */
|
|
130
|
+
.marker-badge .marker-name {
|
|
131
|
+
position: absolute; top: 32px; left: 50%; transform: translateX(-50%);
|
|
132
|
+
white-space: nowrap; font-size: 11px; font-weight: 700; color: #0f172a;
|
|
133
|
+
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff,
|
|
134
|
+
0 0 3px #fff;
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
/* A place pin (search results and other places in the carousel): a
|
|
138
|
+
white disc with the place's Maki icon, black when selected. Numbered
|
|
139
|
+
badges stay for markers whose number means something (visit order). */
|
|
140
|
+
.marker-badge.place {
|
|
141
|
+
width: 30px; height: 30px; background: #fff; color: #0f172a;
|
|
142
|
+
border: none; box-shadow: 0 1px 4px rgba(0,0,0,0.35);
|
|
143
|
+
}
|
|
144
|
+
.marker-badge.place svg { width: 16px; height: 16px; display: block; }
|
|
145
|
+
.marker-badge.place.active {
|
|
146
|
+
background: #0f172a !important; color: #fff !important;
|
|
147
|
+
}
|
|
148
|
+
/* Results carousel. Sits above the Mapbox logo and attribution, which
|
|
149
|
+
must stay visible; the legend moves up above it (see showCarousel). */
|
|
150
|
+
#carousel { position: absolute; left: 0; right: 0; bottom: 36px; z-index: 10; }
|
|
151
|
+
#carousel .card-list {
|
|
152
|
+
list-style: none; margin: 0; padding: 4px 32px 6px;
|
|
153
|
+
display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden;
|
|
154
|
+
scroll-snap-type: x mandatory; scrollbar-width: none;
|
|
155
|
+
}
|
|
156
|
+
#carousel .card-list::-webkit-scrollbar { display: none; }
|
|
157
|
+
#carousel .card {
|
|
158
|
+
flex: 0 0 auto; width: 300px; max-width: calc(100vw - 64px); min-height: 84px;
|
|
159
|
+
scroll-snap-align: center;
|
|
160
|
+
display: flex; align-items: center; gap: 10px; padding: 10px;
|
|
161
|
+
background: var(--ui-surface); color: var(--ui-text);
|
|
162
|
+
border: 2px solid transparent; border-radius: 14px;
|
|
163
|
+
box-shadow: var(--ui-shadow); cursor: pointer;
|
|
164
|
+
}
|
|
165
|
+
#carousel .card.active { border-color: var(--ui-text); }
|
|
166
|
+
/* Added once a photo loads (see loadPanelPhoto), after the text in the
|
|
167
|
+
DOM but shown first. */
|
|
168
|
+
#carousel .card-thumb {
|
|
169
|
+
order: -1; flex: 0 0 auto; width: 64px; height: 64px; border-radius: 10px;
|
|
105
170
|
background-size: cover; background-position: center;
|
|
106
171
|
}
|
|
107
|
-
/*
|
|
108
|
-
|
|
109
|
-
|
|
172
|
+
/* Until a photo loads, or when the place has none: the place's icon
|
|
173
|
+
on a muted tile, so every card has a picture. */
|
|
174
|
+
#carousel .card-thumb.empty {
|
|
175
|
+
background: var(--ui-surface-muted); color: var(--ui-icon-muted);
|
|
176
|
+
display: flex; align-items: center; justify-content: center;
|
|
177
|
+
}
|
|
178
|
+
#carousel .card-thumb.empty svg { width: 28px; height: 28px; display: block; }
|
|
179
|
+
/* Matches the numbered pin. */
|
|
180
|
+
#carousel .card-badge {
|
|
181
|
+
flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%;
|
|
110
182
|
background: #f97316; color: #1a0f04;
|
|
111
183
|
display: flex; align-items: center; justify-content: center;
|
|
112
|
-
font-size:
|
|
184
|
+
font-size: 11px; font-weight: 700;
|
|
113
185
|
}
|
|
114
|
-
#
|
|
115
|
-
#
|
|
186
|
+
#carousel .card-body { flex: 1 1 auto; min-width: 0; }
|
|
187
|
+
#carousel .card-name {
|
|
188
|
+
font-size: 14px; font-weight: 600;
|
|
189
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
190
|
+
}
|
|
191
|
+
#carousel .card-meta, #carousel .card-extra {
|
|
192
|
+
font-size: 12px; color: var(--ui-text-muted); margin-top: 3px;
|
|
193
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
194
|
+
}
|
|
195
|
+
#carousel .hours-open { color: #15803d; font-weight: 600; }
|
|
196
|
+
#carousel .hours-closed { color: #b91c1c; font-weight: 600; }
|
|
197
|
+
:root[data-theme="dark"] #carousel .hours-open { color: #4ade80; }
|
|
198
|
+
:root[data-theme="dark"] #carousel .hours-closed { color: #f87171; }
|
|
116
199
|
</style>
|
|
117
200
|
</head>
|
|
118
201
|
<body>
|
|
119
202
|
<div id="map"></div>
|
|
120
203
|
<div id="summary" style="display:none"></div>
|
|
121
|
-
<div id="
|
|
122
|
-
<div id="
|
|
204
|
+
<div id="legends">
|
|
205
|
+
<div id="legend" style="display:none"></div>
|
|
206
|
+
<div id="traffic-legend" style="display:none"></div>
|
|
207
|
+
</div>
|
|
208
|
+
<div id="carousel" style="display:none"></div>
|
|
123
209
|
<div id="loading">Loading…</div>
|
|
124
210
|
<div id="error" style="display:none"></div>
|
|
125
211
|
${initialDataScript}
|
|
@@ -133,9 +219,11 @@ ${initialDataScript}
|
|
|
133
219
|
var errorEl = document.getElementById('error');
|
|
134
220
|
var summaryEl = document.getElementById('summary');
|
|
135
221
|
var legendEl = document.getElementById('legend');
|
|
136
|
-
var
|
|
137
|
-
|
|
138
|
-
|
|
222
|
+
var trafficLegendEl = document.getElementById('traffic-legend');
|
|
223
|
+
var legendsEl = document.getElementById('legends');
|
|
224
|
+
var carouselEl = document.getElementById('carousel');
|
|
225
|
+
// Populated by buildSearchMiniPayload's marker tagging so the carousel (card
|
|
226
|
+
// <-> pin) and Place Details enrichment can find a marker by mapbox_id
|
|
139
227
|
// without re-querying the map for it.
|
|
140
228
|
var markersById = {};
|
|
141
229
|
|
|
@@ -152,6 +240,45 @@ ${initialDataScript}
|
|
|
152
240
|
var trackedLayerIds = [];
|
|
153
241
|
var trackedSourceIds = [];
|
|
154
242
|
var trackedMarkers = [];
|
|
243
|
+
// Copies of the GeoJSON layers drawn so far. setStyle() drops every
|
|
244
|
+
// source and layer we added (DOM markers survive it), so a style switch
|
|
245
|
+
// from the Satellite button puts these back (see restoreOverlays).
|
|
246
|
+
var drawnLayers = [];
|
|
247
|
+
var currentBaseMapConfig = null;
|
|
248
|
+
var payloadLegend = [];
|
|
249
|
+
var trafficOn = false;
|
|
250
|
+
// Mapbox Traffic v1: live congestion per road segment, refreshed every few
|
|
251
|
+
// minutes. Served from api.mapbox.com, which the page's CSP already allows.
|
|
252
|
+
var TRAFFIC_ID = 'mapbox-live-traffic';
|
|
253
|
+
// Colors from the design's traffic legend (Fast to Slow).
|
|
254
|
+
var TRAFFIC_LEVELS = [
|
|
255
|
+
{ value: 'low', label: 'Light traffic', color: '#00c94e' },
|
|
256
|
+
{ value: 'moderate', label: 'Moderate traffic', color: '#ff9900' },
|
|
257
|
+
{ value: 'heavy', label: 'Heavy traffic', color: '#ff2b3a' },
|
|
258
|
+
{ value: 'severe', label: 'Severe traffic', color: '#8b0a18' }
|
|
259
|
+
];
|
|
260
|
+
var TRAFFIC_MAIN_ROADS = [
|
|
261
|
+
'motorway', 'motorway_link', 'trunk', 'trunk_link',
|
|
262
|
+
'primary', 'secondary', 'tertiary', 'link'
|
|
263
|
+
];
|
|
264
|
+
// 24px filled icons (a globe and a car, as in the design), sized to match
|
|
265
|
+
// the zoom buttons. The globe's lines are cut out in the button color.
|
|
266
|
+
var TOGGLE_ICONS = {
|
|
267
|
+
satellite: '<circle cx="12" cy="12" r="9.5"/>' +
|
|
268
|
+
'<path d="M2.5 12h19M12 2.5a14 14 0 0 1 0 19M12 2.5a14 14 0 0 0 0 19" ' +
|
|
269
|
+
'fill="none" stroke="#fff" stroke-width="1.6"/>',
|
|
270
|
+
traffic: '<path fill-rule="evenodd" d="M5 11l1.6-4.4A2.4 2.4 0 0 1 8.9 5h6.2' +
|
|
271
|
+
'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' +
|
|
272
|
+
'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' +
|
|
273
|
+
'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' +
|
|
274
|
+
'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' +
|
|
275
|
+
'a1.5 1.5 0 1 0 0-3z"/>'
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Set by the layer toggle control, which initMap() adds before the rest
|
|
279
|
+
// of this script has run.
|
|
280
|
+
var satelliteButton = null;
|
|
281
|
+
var trafficButton = null;
|
|
155
282
|
|
|
156
283
|
// --- MCP App postMessage protocol -----------------------------------------
|
|
157
284
|
var messageId = 0;
|
|
@@ -190,6 +317,7 @@ ${initialDataScript}
|
|
|
190
317
|
}
|
|
191
318
|
|
|
192
319
|
window.addEventListener('message', function(event) {
|
|
320
|
+
if (event.source !== window.parent) return;
|
|
193
321
|
var message = event.data;
|
|
194
322
|
if (!message || typeof message !== 'object') return;
|
|
195
323
|
if (message.id !== undefined && pendingRequests.has(message.id)) {
|
|
@@ -203,12 +331,56 @@ ${initialDataScript}
|
|
|
203
331
|
handleToolResult(message.params);
|
|
204
332
|
}
|
|
205
333
|
if (message.method === 'ui/notifications/host-context-changed' &&
|
|
206
|
-
message.params
|
|
207
|
-
|
|
208
|
-
|
|
334
|
+
message.params) {
|
|
335
|
+
// Partial updates: only the fields that changed are sent.
|
|
336
|
+
applyHostContext(message.params);
|
|
337
|
+
if (message.params.displayMode) {
|
|
338
|
+
currentDisplayMode = message.params.displayMode;
|
|
339
|
+
if (map) setTimeout(function() { map.resize(); }, 100);
|
|
340
|
+
}
|
|
209
341
|
}
|
|
210
342
|
});
|
|
211
343
|
|
|
344
|
+
// --- Light/dark theme -----------------------------------------------------
|
|
345
|
+
var rootEl = document.documentElement;
|
|
346
|
+
var hostTheme = null;
|
|
347
|
+
|
|
348
|
+
function applyTheme(theme) {
|
|
349
|
+
if ((theme !== 'light' && theme !== 'dark') || !rootEl) return;
|
|
350
|
+
rootEl.setAttribute('data-theme', theme);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// The host's theme wins over the system's; its style variables
|
|
354
|
+
// (--color-background-primary and so on) feed the --ui-* colors.
|
|
355
|
+
function applyHostContext(ctx) {
|
|
356
|
+
if (!ctx || typeof ctx !== 'object') return;
|
|
357
|
+
if (ctx.theme === 'light' || ctx.theme === 'dark') {
|
|
358
|
+
hostTheme = ctx.theme;
|
|
359
|
+
applyTheme(hostTheme);
|
|
360
|
+
}
|
|
361
|
+
var vars = ctx.styles && ctx.styles.variables;
|
|
362
|
+
if (vars && typeof vars === 'object' && rootEl && rootEl.style &&
|
|
363
|
+
typeof rootEl.style.setProperty === 'function') {
|
|
364
|
+
Object.keys(vars).forEach(function(name) {
|
|
365
|
+
if (/^--[a-z0-9-]+$/i.test(name) && typeof vars[name] === 'string') {
|
|
366
|
+
rootEl.style.setProperty(name, vars[name]);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
window.__applyHostContext = applyHostContext;
|
|
372
|
+
|
|
373
|
+
var darkQuery = typeof window.matchMedia === 'function'
|
|
374
|
+
? window.matchMedia('(prefers-color-scheme: dark)')
|
|
375
|
+
: null;
|
|
376
|
+
function followSystemTheme() {
|
|
377
|
+
if (!hostTheme && darkQuery) applyTheme(darkQuery.matches ? 'dark' : 'light');
|
|
378
|
+
}
|
|
379
|
+
followSystemTheme();
|
|
380
|
+
if (darkQuery && typeof darkQuery.addEventListener === 'function') {
|
|
381
|
+
darkQuery.addEventListener('change', followSystemTheme);
|
|
382
|
+
}
|
|
383
|
+
|
|
212
384
|
// Set once a host answers ui/initialize, even with an error. A page from
|
|
213
385
|
// export_map_html_tool framed by some other site never gets an answer.
|
|
214
386
|
var hostAnswered = false;
|
|
@@ -217,8 +389,9 @@ ${initialDataScript}
|
|
|
217
389
|
appCapabilities: {},
|
|
218
390
|
appInfo: { name: 'Mapbox Map', version: '1.0.0' }
|
|
219
391
|
}).then(
|
|
220
|
-
function() {
|
|
392
|
+
function(result) {
|
|
221
393
|
hostAnswered = true;
|
|
394
|
+
if (result) applyHostContext(result.hostContext);
|
|
222
395
|
sendNotification('ui/notifications/initialized', {});
|
|
223
396
|
},
|
|
224
397
|
function() {
|
|
@@ -273,6 +446,7 @@ ${initialDataScript}
|
|
|
273
446
|
}
|
|
274
447
|
map = new mapboxgl.Map(mapOptions);
|
|
275
448
|
map.addControl(new mapboxgl.NavigationControl(), 'top-left');
|
|
449
|
+
map.addControl(new LayerToggleControl(), 'top-left');
|
|
276
450
|
map.on('load', function() {
|
|
277
451
|
mapLoaded = true;
|
|
278
452
|
if (pendingPayload) {
|
|
@@ -405,6 +579,7 @@ ${initialDataScript}
|
|
|
405
579
|
} else {
|
|
406
580
|
summaryEl.style.display = 'none';
|
|
407
581
|
}
|
|
582
|
+
placeTopLeftControls();
|
|
408
583
|
if (!map) { loadingEl.style.display = 'none'; return; }
|
|
409
584
|
if (mapLoaded) render(payload);
|
|
410
585
|
else pendingPayload = payload;
|
|
@@ -428,8 +603,13 @@ ${initialDataScript}
|
|
|
428
603
|
trackedLayerIds = [];
|
|
429
604
|
trackedSourceIds = [];
|
|
430
605
|
trackedMarkers = [];
|
|
606
|
+
drawnLayers = [];
|
|
607
|
+
// A render without baseMapConfig must not bring back the last one (say,
|
|
608
|
+
// night mode) when the Satellite button switches style.
|
|
609
|
+
currentBaseMapConfig = null;
|
|
431
610
|
markersById = {};
|
|
432
|
-
|
|
611
|
+
carouselEl.style.display = 'none';
|
|
612
|
+
legendsEl.style.bottom = '';
|
|
433
613
|
clearPanel();
|
|
434
614
|
// Place Details responses still in flight belong to the old render.
|
|
435
615
|
panelGeneration += 1;
|
|
@@ -465,10 +645,105 @@ ${initialDataScript}
|
|
|
465
645
|
end: { color: '#ef4444' }
|
|
466
646
|
};
|
|
467
647
|
|
|
648
|
+
// Maki icons (https://github.com/mapbox/maki, CC0), 15x15 path data, for
|
|
649
|
+
// the categories search results most often have. Anything else gets
|
|
650
|
+
// the plain marker.
|
|
651
|
+
var MAKI = {
|
|
652
|
+
'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',
|
|
653
|
+
'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',
|
|
654
|
+
'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',
|
|
655
|
+
'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',
|
|
656
|
+
'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',
|
|
657
|
+
'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',
|
|
658
|
+
'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',
|
|
659
|
+
'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',
|
|
660
|
+
'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',
|
|
661
|
+
'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',
|
|
662
|
+
'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',
|
|
663
|
+
'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',
|
|
664
|
+
'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',
|
|
665
|
+
'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',
|
|
666
|
+
'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',
|
|
667
|
+
'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',
|
|
668
|
+
'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',
|
|
669
|
+
'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',
|
|
670
|
+
'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',
|
|
671
|
+
'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',
|
|
672
|
+
'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',
|
|
673
|
+
'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',
|
|
674
|
+
'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'
|
|
675
|
+
};
|
|
676
|
+
// Category words (Search Box poi_category, or a caller's category) to
|
|
677
|
+
// Maki names, checked in order, so "pizza restaurant" is a pizza.
|
|
678
|
+
var CATEGORY_ICONS = [
|
|
679
|
+
['pizza', 'restaurant-pizza'], ['coffee', 'cafe'], ['cafe', 'cafe'],
|
|
680
|
+
['café', 'cafe'], ['bakery', 'bakery'], ['ice cream', 'ice-cream'],
|
|
681
|
+
['brewery', 'beer'], ['beer', 'beer'], ['pub', 'beer'],
|
|
682
|
+
['bar', 'bar'], ['nightclub', 'bar'], ['wine', 'bar'],
|
|
683
|
+
['fast food', 'fast-food'], ['burger', 'fast-food'],
|
|
684
|
+
['restaurant', 'restaurant'], ['food', 'restaurant'], ['diner', 'restaurant'],
|
|
685
|
+
['hotel', 'lodging'], ['motel', 'lodging'], ['hostel', 'lodging'], ['lodging', 'lodging'],
|
|
686
|
+
['grocery', 'grocery'], ['supermarket', 'grocery'], ['convenience', 'convenience'],
|
|
687
|
+
['gas', 'fuel'], ['fuel', 'fuel'], ['parking', 'parking'],
|
|
688
|
+
['hospital', 'hospital'], ['clinic', 'hospital'], ['pharmacy', 'pharmacy'],
|
|
689
|
+
['park', 'park'], ['garden', 'park'], ['museum', 'museum'], ['gallery', 'museum'],
|
|
690
|
+
['theater', 'theatre'], ['theatre', 'theatre'], ['cinema', 'cinema'], ['movie', 'cinema'],
|
|
691
|
+
['bank', 'bank'], ['atm', 'bank'],
|
|
692
|
+
['shop', 'shop'], ['store', 'shop'], ['boutique', 'shop'],
|
|
693
|
+
['attraction', 'attraction'], ['landmark', 'attraction']
|
|
694
|
+
];
|
|
695
|
+
|
|
696
|
+
// The Maki name for a marker or card: its own maki when we have that
|
|
697
|
+
// icon, else one matched from its category, else the plain marker.
|
|
698
|
+
function placeIcon(maki, category) {
|
|
699
|
+
if (typeof maki === 'string' && MAKI.hasOwnProperty(maki)) return maki;
|
|
700
|
+
if (typeof category === 'string') {
|
|
701
|
+
var words = ' ' + category.toLowerCase().split(/[^a-zà-ÿ]+/).join(' ') + ' ';
|
|
702
|
+
for (var i = 0; i < CATEGORY_ICONS.length; i++) {
|
|
703
|
+
if (words.indexOf(' ' + CATEGORY_ICONS[i][0] + ' ') !== -1) return CATEGORY_ICONS[i][1];
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return 'marker';
|
|
707
|
+
}
|
|
708
|
+
window.__placeIcon = placeIcon;
|
|
709
|
+
|
|
710
|
+
// Only ever called with a key of MAKI, so the markup is all ours.
|
|
711
|
+
function iconSvg(name) {
|
|
712
|
+
var d = MAKI[MAKI.hasOwnProperty(name) ? name : 'marker'];
|
|
713
|
+
return '<svg viewBox="0 0 15 15" aria-hidden="true"><path fill="currentColor" d="' + d + '"/></svg>';
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function buildPlaceElement(icon, color) {
|
|
717
|
+
var el = document.createElement('div');
|
|
718
|
+
el.className = 'marker-badge place';
|
|
719
|
+
// A caller's color (gray for "other results", say) tints the icon.
|
|
720
|
+
if (color) el.style.color = color;
|
|
721
|
+
el.innerHTML = iconSvg(icon);
|
|
722
|
+
return el;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Dark or white, whichever reads better on the badge color (WCAG
|
|
726
|
+
// contrast), so numbers stay legible on orange, green or gray pins.
|
|
727
|
+
var BADGE_DARK_TEXT = '#1a0f04';
|
|
728
|
+
function badgeTextColor(color) {
|
|
729
|
+
var hex = typeof color === 'string' ? color.replace('#', '') : '';
|
|
730
|
+
if (hex.length === 3) hex = hex.split('').map(function(c) { return c + c; }).join('');
|
|
731
|
+
if (!/^[0-9a-f]{6}/i.test(hex)) return '#fff';
|
|
732
|
+
var lum = [0, 2, 4].map(function(i) {
|
|
733
|
+
var c = parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
734
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
735
|
+
});
|
|
736
|
+
var l = 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2];
|
|
737
|
+
// Against white (L=1) vs. against the dark text (L about 0.0055).
|
|
738
|
+
return (1.05 / (l + 0.05)) >= ((l + 0.05) / 0.0555) ? '#fff' : BADGE_DARK_TEXT;
|
|
739
|
+
}
|
|
740
|
+
window.__badgeTextColor = badgeTextColor;
|
|
741
|
+
|
|
468
742
|
function buildBadgeElement(label, color) {
|
|
469
743
|
var el = document.createElement('div');
|
|
470
744
|
el.className = 'marker-badge';
|
|
471
745
|
el.style.background = color;
|
|
746
|
+
el.style.color = badgeTextColor(color);
|
|
472
747
|
el.textContent = label;
|
|
473
748
|
return el;
|
|
474
749
|
}
|
|
@@ -480,7 +755,24 @@ ${initialDataScript}
|
|
|
480
755
|
if (!layer || !layer.id || !layer.data) return;
|
|
481
756
|
map.addSource(layer.id, { type: 'geojson', data: layer.data });
|
|
482
757
|
trackedSourceIds.push(layer.id);
|
|
758
|
+
map.addLayer(layerDefinition(layer));
|
|
759
|
+
trackedLayerIds.push(layer.id);
|
|
760
|
+
drawnLayers.push({
|
|
761
|
+
id: layer.id, type: layer.type, data: layer.data,
|
|
762
|
+
paint: layer.paint, layout: layer.layout, slot: layer.slot
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
var feature = layer.data;
|
|
766
|
+
if (feature.type === 'FeatureCollection' && Array.isArray(feature.features)) {
|
|
767
|
+
feature.features.forEach(function(f) {
|
|
768
|
+
if (f && f.geometry) bbox.extend(f.geometry.coordinates);
|
|
769
|
+
});
|
|
770
|
+
} else if (feature.geometry) {
|
|
771
|
+
bbox.extend(feature.geometry.coordinates);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
483
774
|
|
|
775
|
+
function layerDefinition(layer) {
|
|
484
776
|
var def = {
|
|
485
777
|
id: layer.id,
|
|
486
778
|
type: layer.type,
|
|
@@ -489,17 +781,176 @@ ${initialDataScript}
|
|
|
489
781
|
if (layer.paint) def.paint = layer.paint;
|
|
490
782
|
if (layer.layout) def.layout = layer.layout;
|
|
491
783
|
if (layer.slot) def.slot = layer.slot;
|
|
492
|
-
|
|
493
|
-
|
|
784
|
+
return def;
|
|
785
|
+
}
|
|
494
786
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
787
|
+
// --- Satellite / Traffic toggles -----------------------------------------
|
|
788
|
+
function addTrafficLayer() {
|
|
789
|
+
if (!map.getSource(TRAFFIC_ID)) {
|
|
790
|
+
map.addSource(TRAFFIC_ID, {
|
|
791
|
+
type: 'vector',
|
|
792
|
+
url: 'mapbox://mapbox.mapbox-traffic-v1'
|
|
499
793
|
});
|
|
500
|
-
} else if (feature.geometry) {
|
|
501
|
-
bbox.extend(feature.geometry.coordinates);
|
|
502
794
|
}
|
|
795
|
+
if (map.getLayer(TRAFFIC_ID)) return;
|
|
796
|
+
var colors = ['match', ['get', 'congestion']];
|
|
797
|
+
TRAFFIC_LEVELS.forEach(function(level) { colors.push(level.value, level.color); });
|
|
798
|
+
colors.push('rgba(0,0,0,0)');
|
|
799
|
+
map.addLayer({
|
|
800
|
+
id: TRAFFIC_ID,
|
|
801
|
+
type: 'line',
|
|
802
|
+
source: TRAFFIC_ID,
|
|
803
|
+
'source-layer': 'traffic',
|
|
804
|
+
// Above land and water, below roads' labels and anything we draw.
|
|
805
|
+
slot: 'middle',
|
|
806
|
+
// Light traffic on every side street turns the whole map green, so
|
|
807
|
+
// show it only on main roads; any slowdown shows on every road.
|
|
808
|
+
filter: ['any',
|
|
809
|
+
['!=', ['get', 'congestion'], 'low'],
|
|
810
|
+
['match', ['get', 'class'], TRAFFIC_MAIN_ROADS, true, false]
|
|
811
|
+
],
|
|
812
|
+
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
813
|
+
paint: {
|
|
814
|
+
'line-color': colors,
|
|
815
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1, 15, 2, 18, 4],
|
|
816
|
+
// Each direction is its own line; shift it to its side of the road.
|
|
817
|
+
'line-offset': ['interpolate', ['linear'], ['zoom'], 12, 0.5, 15, 1.5, 18, 3],
|
|
818
|
+
'line-opacity': 0.9
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function removeTrafficLayer() {
|
|
824
|
+
if (map.getLayer(TRAFFIC_ID)) map.removeLayer(TRAFFIC_ID);
|
|
825
|
+
if (map.getSource(TRAFFIC_ID)) map.removeSource(TRAFFIC_ID);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function setTraffic(on) {
|
|
829
|
+
trafficOn = on;
|
|
830
|
+
if (map && mapLoaded) {
|
|
831
|
+
if (on) addTrafficLayer();
|
|
832
|
+
else removeTrafficLayer();
|
|
833
|
+
}
|
|
834
|
+
renderLegend();
|
|
835
|
+
syncToggleButtons();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Switches the base style, then runs afterLoad once the new style is up.
|
|
839
|
+
function switchBaseStyle(styleId, afterLoad) {
|
|
840
|
+
currentBaseStyleId = styleId;
|
|
841
|
+
syncToggleButtons();
|
|
842
|
+
map.setStyle('mapbox://styles/mapbox/' + styleId);
|
|
843
|
+
map.once('style.load', afterLoad);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// After a style switch from the Satellite button: put back what setStyle
|
|
847
|
+
// dropped, without refitting the camera the user may have moved.
|
|
848
|
+
function restoreOverlays() {
|
|
849
|
+
applyBaseMapConfig(currentBaseMapConfig);
|
|
850
|
+
drawnLayers.forEach(function(layer) {
|
|
851
|
+
if (!map.getSource(layer.id)) {
|
|
852
|
+
map.addSource(layer.id, { type: 'geojson', data: layer.data });
|
|
853
|
+
}
|
|
854
|
+
if (!map.getLayer(layer.id)) map.addLayer(layerDefinition(layer));
|
|
855
|
+
});
|
|
856
|
+
if (trafficOn) addTrafficLayer();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function toggleSatellite() {
|
|
860
|
+
if (!map || !mapLoaded) return;
|
|
861
|
+
switchBaseStyle(
|
|
862
|
+
currentBaseStyleId === 'standard-satellite' ? 'standard' : 'standard-satellite',
|
|
863
|
+
restoreOverlays
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// A GL JS control (onAdd returns its element). Built by a returned object
|
|
868
|
+
// rather than prototype methods because initMap() runs before the rest
|
|
869
|
+
// of this script, and addControl calls onAdd right away.
|
|
870
|
+
function LayerToggleControl() {
|
|
871
|
+
return { onAdd: buildLayerToggles, onRemove: function() {} };
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function buildLayerToggles() {
|
|
875
|
+
var container = document.createElement('div');
|
|
876
|
+
container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group layer-toggles';
|
|
877
|
+
satelliteButton = buildToggleButton('Satellite view', TOGGLE_ICONS.satellite, toggleSatellite);
|
|
878
|
+
trafficButton = buildToggleButton('Live traffic', TOGGLE_ICONS.traffic, function() {
|
|
879
|
+
setTraffic(!trafficOn);
|
|
880
|
+
});
|
|
881
|
+
container.appendChild(satelliteButton);
|
|
882
|
+
container.appendChild(trafficButton);
|
|
883
|
+
syncToggleButtons();
|
|
884
|
+
return container;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function buildToggleButton(label, icon, onClick) {
|
|
888
|
+
var button = document.createElement('button');
|
|
889
|
+
button.type = 'button';
|
|
890
|
+
button.title = label;
|
|
891
|
+
if (typeof button.setAttribute === 'function') button.setAttribute('aria-label', label);
|
|
892
|
+
button.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" ' +
|
|
893
|
+
'fill="currentColor" aria-hidden="true">' + icon + '</svg>';
|
|
894
|
+
button.addEventListener('click', onClick);
|
|
895
|
+
return button;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function syncToggleButtons() {
|
|
899
|
+
setPressed(satelliteButton, currentBaseStyleId === 'standard-satellite');
|
|
900
|
+
setPressed(trafficButton, trafficOn);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function setPressed(button, pressed) {
|
|
904
|
+
if (!button) return;
|
|
905
|
+
button.className = pressed ? 'active' : '';
|
|
906
|
+
if (typeof button.setAttribute === 'function') {
|
|
907
|
+
button.setAttribute('aria-pressed', pressed ? 'true' : 'false');
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// The summary chip covers GL JS's top-left control corner, so move the
|
|
912
|
+
// zoom and layer buttons below it while it's shown.
|
|
913
|
+
function placeTopLeftControls() {
|
|
914
|
+
if (!map || typeof map.getContainer !== 'function') return;
|
|
915
|
+
var container = map.getContainer();
|
|
916
|
+
var corner = container && typeof container.querySelector === 'function'
|
|
917
|
+
? container.querySelector('.mapboxgl-ctrl-top-left')
|
|
918
|
+
: null;
|
|
919
|
+
if (!corner) return;
|
|
920
|
+
corner.style.top = summaryEl.style.display === 'block'
|
|
921
|
+
? (summaryEl.offsetHeight + 8) + 'px'
|
|
922
|
+
: '';
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// The payload's legend rows, and below them the traffic scale while
|
|
926
|
+
// traffic is on.
|
|
927
|
+
function renderLegend() {
|
|
928
|
+
renderTrafficLegend();
|
|
929
|
+
var rows = payloadLegend;
|
|
930
|
+
if (rows.length === 0) {
|
|
931
|
+
legendEl.style.display = 'none';
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
legendEl.innerHTML = rows.map(function(row) {
|
|
935
|
+
var op = (typeof row.opacity === 'number') ? row.opacity : 1;
|
|
936
|
+
return '<div class="row"><span class="swatch" style="background:' +
|
|
937
|
+
escapeAttr(row.color) + ';opacity:' + op + '"></span>' +
|
|
938
|
+
escapeText(row.label) + '</div>';
|
|
939
|
+
}).join('');
|
|
940
|
+
legendEl.style.display = 'block';
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function renderTrafficLegend() {
|
|
944
|
+
if (!trafficOn) {
|
|
945
|
+
trafficLegendEl.style.display = 'none';
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
trafficLegendEl.innerHTML = '<span>Fast</span><span class="bar">' +
|
|
949
|
+
TRAFFIC_LEVELS.map(function(level) {
|
|
950
|
+
return '<span title="' + level.label + '" style="background:' +
|
|
951
|
+
level.color + '"></span>';
|
|
952
|
+
}).join('') + '</span><span>Slow</span>';
|
|
953
|
+
trafficLegendEl.style.display = 'flex';
|
|
503
954
|
}
|
|
504
955
|
|
|
505
956
|
function addOneMarker(m, bbox) {
|
|
@@ -510,38 +961,58 @@ ${initialDataScript}
|
|
|
510
961
|
var color = m.color ||
|
|
511
962
|
(STYLE_DEFAULTS[style] && STYLE_DEFAULTS[style].color) || '#3b82f6';
|
|
512
963
|
var marker;
|
|
513
|
-
if (style === '
|
|
964
|
+
if (style === 'place') {
|
|
965
|
+
marker = new mapboxgl.Marker({ element: buildPlaceElement(placeIcon(m.maki, m.category), m.color) });
|
|
966
|
+
} else if (style === 'numbered' || style === 'start' || style === 'end') {
|
|
514
967
|
var label = m.label || (style === 'start' ? 'S' : style === 'end' ? 'E' : '');
|
|
515
968
|
marker = new mapboxgl.Marker({ element: buildBadgeElement(label, color) });
|
|
516
969
|
} else {
|
|
517
970
|
marker = new mapboxgl.Marker({ color: color });
|
|
518
971
|
}
|
|
519
972
|
marker.setLngLat(m.coordinates);
|
|
520
|
-
|
|
973
|
+
// A place with an id gets a carousel card instead of a popup: clicking
|
|
974
|
+
// its pin selects the card (see selectPlace).
|
|
975
|
+
if (m.popup && !m.id) marker.setPopup(new mapboxgl.Popup().setText(m.popup));
|
|
521
976
|
marker.addTo(map);
|
|
522
977
|
trackedMarkers.push(marker);
|
|
523
978
|
// Tagged by buildSearchMiniPayload with the result's mapbox_id so the
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
if (m.id)
|
|
979
|
+
// carousel and Place Details enrichment can look the marker back up
|
|
980
|
+
// later without walking trackedMarkers.
|
|
981
|
+
if (m.id) {
|
|
982
|
+
var badgeEl = typeof marker.getElement === 'function' ? marker.getElement() : null;
|
|
983
|
+
markersById[m.id] = { marker: marker, el: badgeEl, color: color };
|
|
984
|
+
if (badgeEl && m.name) {
|
|
985
|
+
var nameEl = document.createElement('span');
|
|
986
|
+
nameEl.className = 'marker-name';
|
|
987
|
+
nameEl.textContent = m.name;
|
|
988
|
+
badgeEl.appendChild(nameEl);
|
|
989
|
+
}
|
|
990
|
+
if (badgeEl && typeof badgeEl.addEventListener === 'function') {
|
|
991
|
+
var placeId = m.id;
|
|
992
|
+
badgeEl.addEventListener('click', function(e) {
|
|
993
|
+
if (e && typeof e.stopPropagation === 'function') e.stopPropagation();
|
|
994
|
+
selectPlace(placeId, { scrollCard: true, panTo: true });
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
527
998
|
bbox.extend(m.coordinates);
|
|
528
999
|
}
|
|
529
1000
|
|
|
530
|
-
// Guarantees a marker that opts into the
|
|
531
|
-
// a visible number on the map matching its
|
|
1001
|
+
// Guarantees a marker that opts into the carousel (has .id) also gets
|
|
1002
|
+
// a visible number on the map matching its card, unless the caller
|
|
532
1003
|
// already gave it an explicit label. Without this, a caller that builds
|
|
533
1004
|
// plain unlabeled pins for POI markers (observed live: Claude did this
|
|
534
1005
|
// for inline markers, leaving the map showing generic blue pins with no
|
|
535
|
-
// way to tell which
|
|
536
|
-
//
|
|
1006
|
+
// way to tell which card is which) breaks the whole point of the
|
|
1007
|
+
// numbering. Mutates markers in place, so it must run before
|
|
537
1008
|
// addOneMarker draws them.
|
|
538
1009
|
//
|
|
539
1010
|
// Numbering runs across the whole render, not per call: a render can
|
|
540
1011
|
// draw inline markers and then merge in self-fetched search results, and
|
|
541
1012
|
// each set arrives numbered from 1. The first set keeps the caller's
|
|
542
1013
|
// numbers when they're unique (a visit order, say); a later set, or one
|
|
543
|
-
// with repeated numbers, is renumbered to continue the
|
|
544
|
-
// Letter labels ("A") are kept as they are. A place already in the
|
|
1014
|
+
// with repeated numbers, is renumbered to continue the carousel's count.
|
|
1015
|
+
// Letter labels ("A") are kept as they are. A place already in the carousel
|
|
545
1016
|
// this render is dropped rather than drawn and listed twice. Returns the
|
|
546
1017
|
// markers to draw.
|
|
547
1018
|
function applyPanelNumbering(markers) {
|
|
@@ -568,12 +1039,17 @@ ${initialDataScript}
|
|
|
568
1039
|
fresh.forEach(function(m) {
|
|
569
1040
|
if (!m || !m.id) return;
|
|
570
1041
|
if (m.label && (keep || !isWholeNumber(m.label))) return;
|
|
1042
|
+
// A place with no number of its own gets an icon pin, not a number:
|
|
1043
|
+
// the selected pin and card light up together, which is enough to
|
|
1044
|
+
// tell them apart.
|
|
1045
|
+
if (!m.label && m.style !== 'start' && m.style !== 'end') {
|
|
1046
|
+
m.style = 'place';
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
// Otherwise a numbered pin, or a start/end pin, takes the next number.
|
|
571
1050
|
panelCount += 1;
|
|
572
1051
|
var n = String(panelCount);
|
|
573
|
-
if (
|
|
574
|
-
if (m.style !== 'start' && m.style !== 'end') m.style = 'numbered';
|
|
575
|
-
if (!m.color) m.color = '#f97316';
|
|
576
|
-
} else if (typeof m.popup === 'string' && m.popup.indexOf(m.label + '. ') === 0) {
|
|
1052
|
+
if (m.label && typeof m.popup === 'string' && m.popup.indexOf(m.label + '. ') === 0) {
|
|
577
1053
|
// Search popups start with the old number ("3. Name").
|
|
578
1054
|
m.popup = n + '. ' + m.popup.slice(String(m.label).length + 2);
|
|
579
1055
|
}
|
|
@@ -588,26 +1064,33 @@ ${initialDataScript}
|
|
|
588
1064
|
return n > 0 && String(n) === String(label);
|
|
589
1065
|
}
|
|
590
1066
|
|
|
591
|
-
// Bottom padding that keeps fitted data out from under the
|
|
592
|
-
//
|
|
1067
|
+
// Bottom padding that keeps fitted data out from under the carousel and
|
|
1068
|
+
// the legends above it, unless the map is too short to spare the room.
|
|
593
1069
|
function bottomPadding() {
|
|
594
|
-
|
|
595
|
-
|
|
1070
|
+
var bottom = 0;
|
|
1071
|
+
if (carouselEl.style.display === 'block') bottom = carouselTop() + 12;
|
|
1072
|
+
if (legendEl.style.display === 'block' ||
|
|
1073
|
+
trafficLegendEl.style.display === 'flex') {
|
|
1074
|
+
bottom = Math.max(bottom, legendTop() + 12);
|
|
1075
|
+
}
|
|
596
1076
|
var el = document.getElementById('map');
|
|
597
1077
|
var height = (el && el.clientHeight) || window.innerHeight || 0;
|
|
598
|
-
if (bottom <= 50 || (height && height - bottom - 70 <
|
|
1078
|
+
if (bottom <= 50 || (height && height - bottom - 70 < 120)) return 50;
|
|
599
1079
|
return bottom;
|
|
600
1080
|
}
|
|
601
1081
|
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
1082
|
+
// Distance from the bottom of the map to the top of the carousel. 36px
|
|
1083
|
+
// keeps it clear of the Mapbox logo and attribution.
|
|
1084
|
+
var CAROUSEL_BOTTOM = 36;
|
|
1085
|
+
function carouselTop() {
|
|
1086
|
+
return CAROUSEL_BOTTOM + (carouselEl.offsetHeight || 100);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// The legends stack sits 40px up (above the Mapbox logo) until the
|
|
1090
|
+
// carousel pushes it higher.
|
|
1091
|
+
function legendTop() {
|
|
1092
|
+
var bottom = parseInt(legendsEl.style.bottom, 10) || 40;
|
|
1093
|
+
return bottom + (legendsEl.offsetHeight || 0);
|
|
611
1094
|
}
|
|
612
1095
|
|
|
613
1096
|
function mapContainerWidth() {
|
|
@@ -621,7 +1104,7 @@ ${initialDataScript}
|
|
|
621
1104
|
setTimeout(function() {
|
|
622
1105
|
map.resize();
|
|
623
1106
|
map.fitBounds(bounds, {
|
|
624
|
-
padding: { top: 70, bottom: bottomPadding(), left: 30, right:
|
|
1107
|
+
padding: { top: 70, bottom: bottomPadding(), left: 30, right: 30 },
|
|
625
1108
|
duration: 600
|
|
626
1109
|
});
|
|
627
1110
|
}, 60);
|
|
@@ -631,10 +1114,30 @@ ${initialDataScript}
|
|
|
631
1114
|
// the map (does NOT teardown first) and refits to include the new
|
|
632
1115
|
// content. Used once a self-fetched layer (e.g. a directions route)
|
|
633
1116
|
// resolves after the initial render.
|
|
1117
|
+
// Search Box errors carry an object, {"message":{"error":"Invalid
|
|
1118
|
+
// proximity provided..."}}, where the other APIs carry a string; showing
|
|
1119
|
+
// the object printed "[object Object]".
|
|
1120
|
+
function apiErrorText(body, fallback) {
|
|
1121
|
+
var m = body && body.message;
|
|
1122
|
+
if (m && typeof m === 'object') m = m.error || m.message;
|
|
1123
|
+
return typeof m === 'string' && m ? m : fallback;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Self-fetch layers use fixed ids, so two refs of the same kind in one
|
|
1127
|
+
// render (search results for SFO and for Palo Alto) would both add
|
|
1128
|
+
// "selffetch-search-results" and the second addSource() throws.
|
|
1129
|
+
function unusedLayerId(id) {
|
|
1130
|
+
if (!map.getSource(id) && !map.getLayer(id)) return id;
|
|
1131
|
+
var n = 2;
|
|
1132
|
+
while (map.getSource(id + '-' + n) || map.getLayer(id + '-' + n)) n++;
|
|
1133
|
+
return id + '-' + n;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
634
1136
|
function mergeAdditionalPayload(payload) {
|
|
635
1137
|
if (!map) return;
|
|
636
1138
|
var bbox = bboxAccumulator();
|
|
637
1139
|
(Array.isArray(payload.layers) ? payload.layers : []).forEach(function(layer) {
|
|
1140
|
+
if (layer && layer.id) layer = Object.assign({}, layer, { id: unusedLayerId(layer.id) });
|
|
638
1141
|
addOneLayer(layer, bbox);
|
|
639
1142
|
});
|
|
640
1143
|
var markers = applyPanelNumbering(payload.markers);
|
|
@@ -642,14 +1145,16 @@ ${initialDataScript}
|
|
|
642
1145
|
if (payload.summary && summaryEl.style.display === 'none') {
|
|
643
1146
|
summaryEl.textContent = payload.summary;
|
|
644
1147
|
summaryEl.style.display = 'block';
|
|
1148
|
+
placeTopLeftControls();
|
|
645
1149
|
}
|
|
646
|
-
maybeShowSidePanel(markers
|
|
1150
|
+
maybeShowSidePanel(markers);
|
|
647
1151
|
var bounds = bbox.bounds();
|
|
648
1152
|
if (bounds) fitToBounds(bounds);
|
|
649
1153
|
}
|
|
650
1154
|
|
|
651
1155
|
function applyBaseMapConfig(baseMapConfig) {
|
|
652
1156
|
if (!baseMapConfig || typeof baseMapConfig !== 'object') return;
|
|
1157
|
+
currentBaseMapConfig = baseMapConfig;
|
|
653
1158
|
if (typeof map.setConfigProperty !== 'function') return;
|
|
654
1159
|
Object.keys(baseMapConfig).forEach(function(key) {
|
|
655
1160
|
try { map.setConfigProperty('basemap', key, baseMapConfig[key]); } catch (_) { /* not a Standard-based style */ }
|
|
@@ -668,9 +1173,7 @@ ${initialDataScript}
|
|
|
668
1173
|
? 'standard-satellite'
|
|
669
1174
|
: (payload.baseStyle === 'standard' ? 'standard' : null);
|
|
670
1175
|
if (desiredBaseStyleId && desiredBaseStyleId !== currentBaseStyleId) {
|
|
671
|
-
|
|
672
|
-
map.setStyle('mapbox://styles/mapbox/' + desiredBaseStyleId);
|
|
673
|
-
map.once('style.load', function() { renderBody(payload); });
|
|
1176
|
+
switchBaseStyle(desiredBaseStyleId, function() { renderBody(payload); });
|
|
674
1177
|
return;
|
|
675
1178
|
}
|
|
676
1179
|
renderBody(payload);
|
|
@@ -688,21 +1191,14 @@ ${initialDataScript}
|
|
|
688
1191
|
// Covers both inline markers (a caller-composed payload) and self-fetch
|
|
689
1192
|
// markers (merged in later via mergeAdditionalPayload) — see
|
|
690
1193
|
// maybeShowSidePanel below. Whichever path produced id-bearing POI
|
|
691
|
-
// markers gets the same results
|
|
692
|
-
maybeShowSidePanel(markers
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
escapeAttr(row.color) + ';opacity:' + op + '"></span>' +
|
|
700
|
-
escapeText(row.label) + '</div>';
|
|
701
|
-
}).join('');
|
|
702
|
-
legendEl.style.display = 'block';
|
|
703
|
-
} else {
|
|
704
|
-
legendEl.style.display = 'none';
|
|
705
|
-
}
|
|
1194
|
+
// markers gets the same results carousel.
|
|
1195
|
+
maybeShowSidePanel(markers);
|
|
1196
|
+
|
|
1197
|
+
payloadLegend = Array.isArray(payload.legend) ? payload.legend : [];
|
|
1198
|
+
// showTraffic set by the caller wins; left out, keep whatever the user
|
|
1199
|
+
// picked with the Traffic button (a style switch dropped the layer, so
|
|
1200
|
+
// setTraffic puts it back).
|
|
1201
|
+
setTraffic(typeof payload.showTraffic === 'boolean' ? payload.showTraffic : trafficOn);
|
|
706
1202
|
|
|
707
1203
|
loadingEl.style.display = 'none';
|
|
708
1204
|
requestSizeToFit();
|
|
@@ -734,6 +1230,9 @@ ${initialDataScript}
|
|
|
734
1230
|
if (!data) return;
|
|
735
1231
|
var src = map.getSource(payload.defer.layerId);
|
|
736
1232
|
if (src && typeof src.setData === 'function') src.setData(data);
|
|
1233
|
+
drawnLayers.forEach(function(layer) {
|
|
1234
|
+
if (layer.id === payload.defer.layerId) layer.data = data;
|
|
1235
|
+
});
|
|
737
1236
|
},
|
|
738
1237
|
function() { /* ignore — keep what we rendered inline */ }
|
|
739
1238
|
);
|
|
@@ -966,10 +1465,7 @@ ${initialDataScript}
|
|
|
966
1465
|
.json()
|
|
967
1466
|
.catch(function() { return null; })
|
|
968
1467
|
.then(function(body) {
|
|
969
|
-
var msg =
|
|
970
|
-
body && body.message
|
|
971
|
-
? body.message
|
|
972
|
-
: 'Directions API error (' + res.status + ')';
|
|
1468
|
+
var msg = apiErrorText(body, 'Directions API error (' + res.status + ')');
|
|
973
1469
|
throw new Error(msg);
|
|
974
1470
|
});
|
|
975
1471
|
}
|
|
@@ -1141,10 +1637,7 @@ ${initialDataScript}
|
|
|
1141
1637
|
.json()
|
|
1142
1638
|
.catch(function() { return null; })
|
|
1143
1639
|
.then(function(body) {
|
|
1144
|
-
var msg =
|
|
1145
|
-
body && body.message
|
|
1146
|
-
? body.message
|
|
1147
|
-
: 'Isochrone API error (' + res.status + ')';
|
|
1640
|
+
var msg = apiErrorText(body, 'Isochrone API error (' + res.status + ')');
|
|
1148
1641
|
throw new Error(msg);
|
|
1149
1642
|
});
|
|
1150
1643
|
}
|
|
@@ -1280,10 +1773,7 @@ ${initialDataScript}
|
|
|
1280
1773
|
.json()
|
|
1281
1774
|
.catch(function() { return null; })
|
|
1282
1775
|
.then(function(body) {
|
|
1283
|
-
var msg =
|
|
1284
|
-
body && body.message
|
|
1285
|
-
? body.message
|
|
1286
|
-
: 'Map Matching API error (' + res.status + ')';
|
|
1776
|
+
var msg = apiErrorText(body, 'Map Matching API error (' + res.status + ')');
|
|
1287
1777
|
throw new Error(msg);
|
|
1288
1778
|
});
|
|
1289
1779
|
}
|
|
@@ -1393,29 +1883,31 @@ ${initialDataScript}
|
|
|
1393
1883
|
var features = [];
|
|
1394
1884
|
points.forEach(function(f, i) {
|
|
1395
1885
|
var props = f.properties || {};
|
|
1396
|
-
var popupParts = [
|
|
1886
|
+
var popupParts = [props.name || 'Result'];
|
|
1397
1887
|
var addr = props.full_address || props.place_formatted;
|
|
1398
1888
|
if (addr) popupParts.push(addr);
|
|
1399
|
-
|
|
1400
|
-
|
|
1889
|
+
// Without a proximity the API measures distance from the caller's IP,
|
|
1890
|
+
// which on a hosted server is the user's browser or nowhere useful.
|
|
1891
|
+
var distance = proximity && typeof props.distance === 'number' ? props.distance : undefined;
|
|
1892
|
+
if (distance !== undefined) {
|
|
1893
|
+
popupParts.push(Math.round(distance) + ' m');
|
|
1401
1894
|
}
|
|
1402
1895
|
var coords = f.geometry.coordinates;
|
|
1403
1896
|
// id/name/category/distanceMeters are the same generic POI fields an
|
|
1404
1897
|
// inline (hand-composed) marker can carry — see maybeShowSidePanel,
|
|
1405
|
-
// which builds the results
|
|
1406
|
-
// mapbox_id is missing on the rare result that lacks one; the
|
|
1898
|
+
// which builds the results carousel from either source identically.
|
|
1899
|
+
// mapbox_id is missing on the rare result that lacks one; the carousel
|
|
1407
1900
|
// just skips that row rather than show an unclickable/unenrichable
|
|
1408
1901
|
// entry.
|
|
1409
1902
|
markers.push({
|
|
1410
1903
|
id: props.mapbox_id,
|
|
1411
1904
|
coordinates: coords,
|
|
1412
|
-
style: '
|
|
1413
|
-
|
|
1414
|
-
color: '#f97316',
|
|
1905
|
+
style: 'place',
|
|
1906
|
+
maki: props.maki,
|
|
1415
1907
|
popup: popupParts.join(' — '),
|
|
1416
1908
|
name: props.name || ('Result ' + (i + 1)),
|
|
1417
1909
|
category: Array.isArray(props.poi_category) ? props.poi_category[0] : undefined,
|
|
1418
|
-
distanceMeters:
|
|
1910
|
+
distanceMeters: distance
|
|
1419
1911
|
});
|
|
1420
1912
|
features.push({
|
|
1421
1913
|
type: 'Feature',
|
|
@@ -1435,11 +1927,11 @@ ${initialDataScript}
|
|
|
1435
1927
|
data: { type: 'FeatureCollection', features: features },
|
|
1436
1928
|
paint: {
|
|
1437
1929
|
'circle-radius': 6,
|
|
1438
|
-
'circle-color': '#
|
|
1439
|
-
'circle-opacity': 0.
|
|
1930
|
+
'circle-color': '#0f172a',
|
|
1931
|
+
'circle-opacity': 0.1,
|
|
1440
1932
|
'circle-stroke-width': 1,
|
|
1441
|
-
'circle-stroke-color': '#
|
|
1442
|
-
'circle-stroke-opacity': 0.
|
|
1933
|
+
'circle-stroke-color': '#0f172a',
|
|
1934
|
+
'circle-stroke-opacity': 0.25
|
|
1443
1935
|
}
|
|
1444
1936
|
}
|
|
1445
1937
|
] : [];
|
|
@@ -1447,27 +1939,27 @@ ${initialDataScript}
|
|
|
1447
1939
|
return { summary: summary, layers: layers, markers: markers };
|
|
1448
1940
|
}
|
|
1449
1941
|
|
|
1450
|
-
// ---
|
|
1942
|
+
// --- Results carousel: one card per place + Place Details enrichment ---
|
|
1451
1943
|
// Triggered generically by maybeShowSidePanel for ANY marker set that
|
|
1452
1944
|
// includes id-bearing entries — self-fetch search/category-search
|
|
1453
1945
|
// results (via mergeAdditionalPayload) and hand-composed inline markers
|
|
1454
1946
|
// from render_map_tool's own "markers" input (via renderBody) both funnel
|
|
1455
1947
|
// through the same path. Renders the initial (Search Box-only, or
|
|
1456
|
-
// whatever the caller already provided)
|
|
1457
|
-
// never blocks on the slower enrichment call.
|
|
1458
|
-
// shown as the panel heading (e.g. the query, category, or the payload's
|
|
1459
|
-
// own summary).
|
|
1948
|
+
// whatever the caller already provided) cards immediately so the
|
|
1949
|
+
// carousel never blocks on the slower enrichment call.
|
|
1460
1950
|
function derivePanelItems(markers) {
|
|
1461
1951
|
if (!Array.isArray(markers)) return [];
|
|
1462
1952
|
var items = [];
|
|
1463
1953
|
markers.forEach(function(m) {
|
|
1464
1954
|
if (!m || !m.id) return;
|
|
1465
|
-
// The pin's own label, so a
|
|
1955
|
+
// The pin's own label, so a card reads the same as its pin ("A" too).
|
|
1956
|
+
// A place pin has none, and neither does its card.
|
|
1466
1957
|
var badge = isWholeNumber(m.label) ? parseInt(m.label, 10)
|
|
1467
|
-
: (m.label ? String(m.label) :
|
|
1958
|
+
: (m.label ? String(m.label) : null);
|
|
1468
1959
|
items.push({
|
|
1469
1960
|
id: m.id,
|
|
1470
1961
|
number: badge,
|
|
1962
|
+
icon: placeIcon(m.maki, m.category),
|
|
1471
1963
|
name: m.name || ('Result ' + (items.length + 1)),
|
|
1472
1964
|
category: m.category,
|
|
1473
1965
|
distanceMeters: typeof m.distanceMeters === 'number' ? m.distanceMeters : undefined,
|
|
@@ -1479,16 +1971,16 @@ ${initialDataScript}
|
|
|
1479
1971
|
// Exposed so tests can exercise the pure derivation logic without a DOM.
|
|
1480
1972
|
window.__derivePanelItems = derivePanelItems;
|
|
1481
1973
|
|
|
1482
|
-
// Adds
|
|
1483
|
-
// those. A response is applied only if no newer render has started
|
|
1484
|
-
// (panelGeneration), so a slow lookup can't write into
|
|
1485
|
-
function maybeShowSidePanel(markers
|
|
1974
|
+
// Adds cards for places not yet in this render's carousel and enriches
|
|
1975
|
+
// just those. A response is applied only if no newer render has started
|
|
1976
|
+
// since (panelGeneration), so a slow lookup can't write into newer cards.
|
|
1977
|
+
function maybeShowSidePanel(markers) {
|
|
1486
1978
|
var items = derivePanelItems(markers).filter(function(it) {
|
|
1487
1979
|
return !panelIds[it.id];
|
|
1488
1980
|
});
|
|
1489
1981
|
if (items.length === 0) return;
|
|
1490
1982
|
items.forEach(function(it) { panelIds[it.id] = true; });
|
|
1491
|
-
|
|
1983
|
+
appendCards(items);
|
|
1492
1984
|
var generation = panelGeneration;
|
|
1493
1985
|
fetchPlaceDetailsBatch(
|
|
1494
1986
|
items.map(function(it) { return it.id; }),
|
|
@@ -1499,109 +1991,334 @@ ${initialDataScript}
|
|
|
1499
1991
|
});
|
|
1500
1992
|
}
|
|
1501
1993
|
|
|
1502
|
-
// Keyed by mapbox_id -> { li, meta } elements, so enrichSidePanel
|
|
1503
|
-
// update a
|
|
1504
|
-
//
|
|
1994
|
+
// Keyed by mapbox_id -> { li, meta, body } elements, so enrichSidePanel
|
|
1995
|
+
// can update a card directly once Place Details resolves, without
|
|
1996
|
+
// re-parsing markup back into a queryable DOM (innerHTML + querySelector
|
|
1505
1997
|
// round-trips don't work in every MCP Apps host's iframe sandbox, and
|
|
1506
1998
|
// building real nodes up front is simpler to reason about either way).
|
|
1507
|
-
// Reset by clearPanel, same lifecycle as the
|
|
1999
|
+
// Reset by clearPanel, same lifecycle as the carousel itself.
|
|
1508
2000
|
var panelRowsById = {};
|
|
1509
2001
|
// Bumped by teardown(), so responses from an earlier render are dropped.
|
|
1510
2002
|
var panelGeneration = 0;
|
|
1511
2003
|
// Id-bearing markers numbered so far this render, and the ids listed.
|
|
1512
2004
|
var panelCount = 0;
|
|
1513
2005
|
var panelIds = {};
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
var
|
|
1517
|
-
|
|
2006
|
+
var cardListEl = null;
|
|
2007
|
+
var cardOrder = [];
|
|
2008
|
+
var activePlaceId = null;
|
|
2009
|
+
|
|
2010
|
+
// classList when there is one: GL JS adds its own classes to a marker's
|
|
2011
|
+
// element, which assigning className would wipe out.
|
|
2012
|
+
function setActiveClass(el, on) {
|
|
2013
|
+
if (!el) return;
|
|
2014
|
+
if (el.classList && typeof el.classList.toggle === 'function') {
|
|
2015
|
+
el.classList.toggle('active', on);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
var names = String(el.className || '').split(' ').filter(function(c) {
|
|
2019
|
+
return c && c !== 'active';
|
|
2020
|
+
});
|
|
2021
|
+
if (on) names.push('active');
|
|
2022
|
+
el.className = names.join(' ');
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
// Highlights one place: its card gets a border and its pin inverts and
|
|
2026
|
+
// shows the name. scrollCard brings the card into view (a pin was
|
|
2027
|
+
// clicked); panTo moves the map when the pin is hidden (a card was
|
|
2028
|
+
// picked).
|
|
2029
|
+
function selectPlace(id, opts) {
|
|
2030
|
+
var row = panelRowsById[id];
|
|
2031
|
+
if (!row) return;
|
|
2032
|
+
opts = opts || {};
|
|
2033
|
+
if (activePlaceId !== id) {
|
|
2034
|
+
if (activePlaceId) {
|
|
2035
|
+
var prevRow = panelRowsById[activePlaceId];
|
|
2036
|
+
var prevPin = markersById[activePlaceId];
|
|
2037
|
+
if (prevRow) setActiveClass(prevRow.li, false);
|
|
2038
|
+
if (prevPin && prevPin.el) {
|
|
2039
|
+
setActiveClass(prevPin.el, false);
|
|
2040
|
+
prevPin.el.style.zIndex = '';
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
activePlaceId = id;
|
|
2044
|
+
setActiveClass(row.li, true);
|
|
2045
|
+
var pin = markersById[id];
|
|
2046
|
+
if (pin && pin.el) {
|
|
2047
|
+
setActiveClass(pin.el, true);
|
|
2048
|
+
pin.el.style.zIndex = '2';
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (opts.scrollCard && typeof row.li.scrollIntoView === 'function') {
|
|
2052
|
+
row.li.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
2053
|
+
}
|
|
2054
|
+
if (opts.panTo) panToIfHidden(id);
|
|
2055
|
+
}
|
|
2056
|
+
window.__selectPlace = selectPlace;
|
|
1518
2057
|
|
|
1519
|
-
|
|
2058
|
+
// Moves the map only when the pin, or the name label under it, is off
|
|
2059
|
+
// screen or under the carousel, so swiping through nearby results
|
|
2060
|
+
// doesn't keep shifting it.
|
|
2061
|
+
var PIN_LABEL_ROOM = 52;
|
|
2062
|
+
function panToIfHidden(id) {
|
|
2063
|
+
var entry = markersById[id];
|
|
2064
|
+
if (!entry || !map || typeof map.project !== 'function') return;
|
|
2065
|
+
var lngLat = entry.marker.getLngLat();
|
|
2066
|
+
var p = map.project(lngLat);
|
|
2067
|
+
var el = document.getElementById('map');
|
|
2068
|
+
var width = (el && el.clientWidth) || 0;
|
|
2069
|
+
var height = (el && el.clientHeight) || 0;
|
|
2070
|
+
if (!width || !height) return;
|
|
2071
|
+
var visibleBottom = height - carouselTop() - PIN_LABEL_ROOM;
|
|
2072
|
+
if (p.x >= 24 && p.x <= width - 24 && p.y >= 24 && p.y <= visibleBottom) return;
|
|
2073
|
+
// Just far enough to bring it 24px inside the visible area, so the
|
|
2074
|
+
// pins around it mostly stay in view.
|
|
2075
|
+
var x = Math.min(Math.max(p.x, 48), width - 48);
|
|
2076
|
+
var y = Math.min(Math.max(p.y, 48), Math.max(48, visibleBottom - 24));
|
|
2077
|
+
map.panBy([p.x - x, p.y - y], { duration: 500 });
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function buildCard(it) {
|
|
1520
2081
|
var li = document.createElement('li');
|
|
1521
|
-
li.className = '
|
|
2082
|
+
li.className = 'card';
|
|
1522
2083
|
|
|
1523
|
-
var badge =
|
|
1524
|
-
|
|
1525
|
-
|
|
2084
|
+
var badge = null;
|
|
2085
|
+
if (it.number !== null && it.number !== undefined) {
|
|
2086
|
+
badge = document.createElement('span');
|
|
2087
|
+
badge.className = 'card-badge';
|
|
2088
|
+
badge.textContent = String(it.number);
|
|
2089
|
+
}
|
|
1526
2090
|
// Match the pin when the caller colored it (e.g. gray for "other
|
|
1527
2091
|
// results" next to a legend); the default orange is in the CSS.
|
|
1528
|
-
if (typeof it.color === 'string' && it.color.toLowerCase() !== '#f97316' &&
|
|
2092
|
+
if (badge && typeof it.color === 'string' && it.color.toLowerCase() !== '#f97316' &&
|
|
1529
2093
|
(/^#[0-9a-f]{3,8}$/i).test(it.color)) {
|
|
1530
2094
|
badge.style.background = it.color;
|
|
1531
|
-
badge.style.color =
|
|
2095
|
+
badge.style.color = badgeTextColor(it.color);
|
|
1532
2096
|
}
|
|
1533
2097
|
|
|
1534
2098
|
var name = document.createElement('div');
|
|
1535
|
-
name.className = '
|
|
2099
|
+
name.className = 'card-name';
|
|
1536
2100
|
name.textContent = it.name;
|
|
1537
2101
|
|
|
1538
|
-
var metaParts = [];
|
|
1539
|
-
if (it.category) metaParts.push(it.category);
|
|
1540
|
-
if (typeof it.distanceMeters === 'number') {
|
|
1541
|
-
metaParts.push(Math.round(it.distanceMeters) + ' m');
|
|
1542
|
-
}
|
|
1543
2102
|
var meta = document.createElement('div');
|
|
1544
|
-
meta.className = '
|
|
1545
|
-
meta.textContent =
|
|
2103
|
+
meta.className = 'card-meta';
|
|
2104
|
+
meta.textContent = cardMetaText(it, null);
|
|
1546
2105
|
|
|
1547
2106
|
var body = document.createElement('div');
|
|
2107
|
+
body.className = 'card-body';
|
|
1548
2108
|
body.appendChild(name);
|
|
1549
2109
|
body.appendChild(meta);
|
|
1550
|
-
|
|
2110
|
+
// Placeholder picture, replaced by the photo if one loads (see
|
|
2111
|
+
// loadPanelPhoto). Shown first by CSS order.
|
|
2112
|
+
var thumb = document.createElement('span');
|
|
2113
|
+
thumb.className = 'card-thumb empty';
|
|
2114
|
+
thumb.innerHTML = iconSvg(it.icon);
|
|
2115
|
+
if (typeof thumb.setAttribute === 'function') {
|
|
2116
|
+
thumb.setAttribute('role', 'img');
|
|
2117
|
+
thumb.setAttribute('aria-label', it.name);
|
|
2118
|
+
}
|
|
2119
|
+
if (badge) li.appendChild(badge);
|
|
1551
2120
|
li.appendChild(body);
|
|
2121
|
+
li.appendChild(thumb);
|
|
1552
2122
|
|
|
1553
2123
|
li.addEventListener('click', function() {
|
|
1554
|
-
|
|
1555
|
-
if (entry) map.flyTo({ center: entry.marker.getLngLat(), zoom: 15, duration: 500 });
|
|
2124
|
+
selectPlace(it.id, { scrollCard: true, panTo: true });
|
|
1556
2125
|
});
|
|
1557
2126
|
|
|
1558
|
-
panelRowsById[it.id] = { li: li, meta: meta };
|
|
2127
|
+
panelRowsById[it.id] = { li: li, meta: meta, body: body, thumb: thumb, item: it };
|
|
1559
2128
|
return li;
|
|
1560
2129
|
}
|
|
1561
2130
|
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2131
|
+
// "Cafe · $$ · 120 m". Search Box categories come lowercase.
|
|
2132
|
+
function cardMetaText(it, price) {
|
|
2133
|
+
var parts = [];
|
|
2134
|
+
if (it.category) parts.push(it.category.charAt(0).toUpperCase() + it.category.slice(1));
|
|
2135
|
+
if (price) parts.push(price);
|
|
2136
|
+
if (typeof it.distanceMeters === 'number') {
|
|
2137
|
+
parts.push(Math.round(it.distanceMeters) + ' m');
|
|
2138
|
+
}
|
|
2139
|
+
return parts.join(' · ');
|
|
1566
2140
|
}
|
|
1567
2141
|
|
|
1568
|
-
|
|
1569
|
-
|
|
2142
|
+
// Places API attributes.price_level as dollar signs. Unknown levels show
|
|
2143
|
+
// nothing rather than a guess.
|
|
2144
|
+
var PRICE_SIGNS = { 'cheap': '$', 'moderate': '$$', 'expensive': '$$$', 'very expensive': '$$$$' };
|
|
2145
|
+
function priceSigns(level) {
|
|
2146
|
+
if (typeof level !== 'string') return null;
|
|
2147
|
+
return PRICE_SIGNS[level.trim().toLowerCase()] || null;
|
|
1570
2148
|
}
|
|
2149
|
+
window.__priceSigns = priceSigns;
|
|
1571
2150
|
|
|
1572
|
-
function
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
2151
|
+
function clearPanel() {
|
|
2152
|
+
while (carouselEl.firstChild) carouselEl.removeChild(carouselEl.firstChild);
|
|
2153
|
+
panelRowsById = {};
|
|
2154
|
+
cardListEl = null;
|
|
2155
|
+
cardOrder = [];
|
|
2156
|
+
activePlaceId = null;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
// Swiping selects whichever card ends up in the middle.
|
|
2160
|
+
var scrollTimer = null;
|
|
2161
|
+
function onCardScroll() {
|
|
2162
|
+
if (scrollTimer) clearTimeout(scrollTimer);
|
|
2163
|
+
scrollTimer = setTimeout(function() {
|
|
2164
|
+
scrollTimer = null;
|
|
2165
|
+
if (!cardListEl) return;
|
|
2166
|
+
var center = (cardListEl.scrollLeft || 0) + (cardListEl.clientWidth || 0) / 2;
|
|
2167
|
+
var best = null;
|
|
2168
|
+
var bestDist = Infinity;
|
|
2169
|
+
cardOrder.forEach(function(id) {
|
|
2170
|
+
var row = panelRowsById[id];
|
|
2171
|
+
if (!row) return;
|
|
2172
|
+
var mid = (row.li.offsetLeft || 0) + (row.li.offsetWidth || 0) / 2;
|
|
2173
|
+
var d = Math.abs(mid - center);
|
|
2174
|
+
if (d < bestDist) { bestDist = d; best = id; }
|
|
2175
|
+
});
|
|
2176
|
+
if (best && best !== activePlaceId) selectPlace(best, { panTo: true });
|
|
2177
|
+
}, 120);
|
|
1576
2178
|
}
|
|
1577
2179
|
|
|
1578
|
-
// Creates the
|
|
1579
|
-
//
|
|
1580
|
-
// markers), so every numbered pin has a
|
|
1581
|
-
|
|
2180
|
+
// Creates the carousel on the render's first id-bearing markers, and adds
|
|
2181
|
+
// cards for later ones (e.g. search results merged in after inline
|
|
2182
|
+
// markers), so every numbered pin has a card. The first card starts
|
|
2183
|
+
// selected.
|
|
2184
|
+
function appendCards(items) {
|
|
1582
2185
|
if (!Array.isArray(items) || items.length === 0) return;
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2186
|
+
var created = false;
|
|
2187
|
+
if (!cardListEl) {
|
|
2188
|
+
cardListEl = document.createElement('ul');
|
|
2189
|
+
cardListEl.className = 'card-list';
|
|
2190
|
+
cardListEl.addEventListener('scroll', onCardScroll);
|
|
2191
|
+
carouselEl.appendChild(cardListEl);
|
|
2192
|
+
carouselEl.style.display = 'block';
|
|
2193
|
+
created = true;
|
|
2194
|
+
}
|
|
2195
|
+
var list = cardListEl;
|
|
2196
|
+
items.forEach(function(it) {
|
|
2197
|
+
list.appendChild(buildCard(it));
|
|
2198
|
+
cardOrder.push(it.id);
|
|
2199
|
+
});
|
|
2200
|
+
// The legends (bottom-left) move up above the cards.
|
|
2201
|
+
legendsEl.style.bottom = (carouselTop() + 8) + 'px';
|
|
2202
|
+
if (created) selectPlace(items[0].id);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Opening hours from a Places API opening_hours string, which uses OSM
|
|
2206
|
+
// syntax: "Mo-Fr 07:00-19:00; Sa,Su 08:00-17:00", "Mo 09:00-12:00,13:00-17:00",
|
|
2207
|
+
// "Su off", "24/7". Returns null for anything else (holidays, months, week
|
|
2208
|
+
// numbers), so a card shows no hours rather than wrong ones.
|
|
2209
|
+
//
|
|
2210
|
+
// With the place's time zone (Places telemetry.tags.timezone) we know its
|
|
2211
|
+
// clock, so the card says whether it's open now: { state: 'open', text:
|
|
2212
|
+
// 'until 8:30 PM' } or { state: 'closed', text: '· opens 7:30 AM' }.
|
|
2213
|
+
// Without one we only know the viewer's day, so the card shows today's
|
|
2214
|
+
// hours: { state: 'hours', text: 'Open 7:30 AM–3:00 PM' }. Times use the
|
|
2215
|
+
// viewer's locale (7:30–15:00 in en-GB).
|
|
2216
|
+
var OSM_WEEK = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'];
|
|
2217
|
+
var INTL_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
2218
|
+
var HOURS_RANGE = /^[0-9]{2}:[0-9]{2}-[0-9]{2}:[0-9]{2}$/;
|
|
2219
|
+
function osmDaysInclude(days, day) {
|
|
2220
|
+
var parts = days.split(',');
|
|
2221
|
+
var hit = false;
|
|
2222
|
+
for (var i = 0; i < parts.length; i++) {
|
|
2223
|
+
var ends = parts[i].split('-');
|
|
2224
|
+
var a = OSM_WEEK.indexOf(ends[0]);
|
|
2225
|
+
var b = ends.length === 2 ? OSM_WEEK.indexOf(ends[1]) : a;
|
|
2226
|
+
if (a === -1 || b === -1 || ends.length > 2) return null;
|
|
2227
|
+
if (a <= b ? (day >= a && day <= b) : (day >= a || day <= b)) hit = true;
|
|
2228
|
+
}
|
|
2229
|
+
return hit;
|
|
2230
|
+
}
|
|
2231
|
+
function toMinutes(hhmm) {
|
|
2232
|
+
return parseInt(hhmm.slice(0, 2), 10) * 60 + parseInt(hhmm.slice(3, 5), 10);
|
|
2233
|
+
}
|
|
2234
|
+
// A day's opening ranges as [startMinute, endMinute] pairs (end <= start
|
|
2235
|
+
// runs past midnight), 'always' for 24/7, or null when unparseable.
|
|
2236
|
+
function osmDayRanges(spec, day) {
|
|
2237
|
+
if (spec === '24/7') return 'always';
|
|
2238
|
+
var result = [];
|
|
2239
|
+
var rules = spec.split(';');
|
|
2240
|
+
for (var i = 0; i < rules.length; i++) {
|
|
2241
|
+
var rule = rules[i].trim();
|
|
2242
|
+
if (!rule) continue;
|
|
2243
|
+
var space = rule.indexOf(' ');
|
|
2244
|
+
if (space === -1) return null;
|
|
2245
|
+
var applies = osmDaysInclude(rule.slice(0, space), day);
|
|
2246
|
+
if (applies === null) return null;
|
|
2247
|
+
var times = rule.slice(space + 1).trim();
|
|
2248
|
+
var ranges = times === 'off' || times === 'closed' ? [] : times.split(',');
|
|
2249
|
+
var parsed = [];
|
|
2250
|
+
for (var j = 0; j < ranges.length; j++) {
|
|
2251
|
+
var r = ranges[j].trim();
|
|
2252
|
+
if (!HOURS_RANGE.test(r)) return null;
|
|
2253
|
+
parsed.push([toMinutes(r.slice(0, 5)), toMinutes(r.slice(6, 11))]);
|
|
1598
2254
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
2255
|
+
// In OSM a later rule for the same day overrides an earlier one.
|
|
2256
|
+
if (applies) result = parsed;
|
|
1601
2257
|
}
|
|
1602
|
-
|
|
1603
|
-
items.forEach(function(it) { list.appendChild(buildPanelRow(it)); });
|
|
2258
|
+
return result;
|
|
1604
2259
|
}
|
|
2260
|
+
// The place's weekday (0 = Monday) and minute of the day, or null for a
|
|
2261
|
+
// missing or unknown time zone.
|
|
2262
|
+
function clockIn(timeZone, now) {
|
|
2263
|
+
if (typeof timeZone !== 'string' || !timeZone) return null;
|
|
2264
|
+
var parts;
|
|
2265
|
+
try {
|
|
2266
|
+
parts = new Intl.DateTimeFormat('en-US', {
|
|
2267
|
+
timeZone: timeZone, weekday: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
|
|
2268
|
+
}).formatToParts(now);
|
|
2269
|
+
} catch (e) {
|
|
2270
|
+
return null;
|
|
2271
|
+
}
|
|
2272
|
+
var p = {};
|
|
2273
|
+
parts.forEach(function(part) { p[part.type] = part.value; });
|
|
2274
|
+
var day = INTL_WEEK.indexOf(p.weekday);
|
|
2275
|
+
if (day === -1) return null;
|
|
2276
|
+
return { day: day, minutes: (parseInt(p.hour, 10) % 24) * 60 + parseInt(p.minute, 10) };
|
|
2277
|
+
}
|
|
2278
|
+
function formatMinutes(minutes, locale) {
|
|
2279
|
+
var d = new Date(2000, 0, 1, Math.floor(minutes / 60) % 24, minutes % 60);
|
|
2280
|
+
try {
|
|
2281
|
+
return new Intl.DateTimeFormat(locale, { hour: 'numeric', minute: '2-digit' }).format(d);
|
|
2282
|
+
} catch (e) {
|
|
2283
|
+
return new Intl.DateTimeFormat(undefined, { hour: 'numeric', minute: '2-digit' }).format(d);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
function hoursToday(spec, now, timeZone, locale) {
|
|
2287
|
+
if (typeof spec !== 'string') return null;
|
|
2288
|
+
var text = spec.trim();
|
|
2289
|
+
if (!text) return null;
|
|
2290
|
+
var clock = clockIn(timeZone, now);
|
|
2291
|
+
var day = clock ? clock.day : (now.getDay() + 6) % 7;
|
|
2292
|
+
var today = osmDayRanges(text, day);
|
|
2293
|
+
if (today === null) return null;
|
|
2294
|
+
if (today === 'always') return { state: 'open', text: '24 hours' };
|
|
2295
|
+
var fmt = function(m) { return formatMinutes(m, locale); };
|
|
2296
|
+
if (!clock) {
|
|
2297
|
+
if (today.length === 0) return { state: 'closed', text: 'today' };
|
|
2298
|
+
return {
|
|
2299
|
+
state: 'hours',
|
|
2300
|
+
text: 'Open ' + today.map(function(r) { return fmt(r[0]) + '–' + fmt(r[1]); }).join(', ')
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
var t = clock.minutes;
|
|
2304
|
+
// Still open from a range that started yesterday and runs past midnight.
|
|
2305
|
+
var yesterday = osmDayRanges(text, (day + 6) % 7) || [];
|
|
2306
|
+
for (var i = 0; i < yesterday.length; i++) {
|
|
2307
|
+
if (yesterday[i][1] <= yesterday[i][0] && t < yesterday[i][1]) {
|
|
2308
|
+
return { state: 'open', text: 'until ' + fmt(yesterday[i][1]) };
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
var next = null;
|
|
2312
|
+
for (var j = 0; j < today.length; j++) {
|
|
2313
|
+
var s = today[j][0];
|
|
2314
|
+
var e = today[j][1];
|
|
2315
|
+
var overnight = e <= s;
|
|
2316
|
+
if (t >= s && (overnight || t < e)) return { state: 'open', text: 'until ' + fmt(e) };
|
|
2317
|
+
if (t < s && (next === null || s < next)) next = s;
|
|
2318
|
+
}
|
|
2319
|
+
return { state: 'closed', text: next === null ? '' : '· opens ' + fmt(next) };
|
|
2320
|
+
}
|
|
2321
|
+
window.__hoursToday = hoursToday;
|
|
1605
2322
|
|
|
1606
2323
|
// Search Box results mix Mapbox-native POI ids (decode to
|
|
1607
2324
|
// "urn:mbxpoi:<uuid>") with OSM-sourced ones (decode to
|
|
@@ -1627,7 +2344,7 @@ ${initialDataScript}
|
|
|
1627
2344
|
|
|
1628
2345
|
// Places API Details endpoint — batch form. Mirrors the single-record
|
|
1629
2346
|
// GET src/tools/place-details-tool/PlaceDetailsTool.ts calls server-side,
|
|
1630
|
-
// but the
|
|
2347
|
+
// but the carousel needs details for every result at once, so this uses the
|
|
1631
2348
|
// POST /details/retrieve batch form (up to 100 ids) instead of one
|
|
1632
2349
|
// request per pin. Docs: https://docs.mapbox.com/api/search/places/
|
|
1633
2350
|
// Like every other self-fetch call, this runs client-side against the
|
|
@@ -1662,18 +2379,18 @@ ${initialDataScript}
|
|
|
1662
2379
|
});
|
|
1663
2380
|
};
|
|
1664
2381
|
return post(compatibleIds.slice(0, 100), false)
|
|
1665
|
-
// Enrichment is a nice-to-have on top of
|
|
2382
|
+
// Enrichment is a nice-to-have on top of already-rendered cards —
|
|
1666
2383
|
// Public Preview quota (1,000 records/month) or a transient error
|
|
1667
2384
|
// here should never surface as a map error, just leave the
|
|
1668
|
-
// Search-Box-only
|
|
2385
|
+
// Search-Box-only cards as they are.
|
|
1669
2386
|
.catch(function() { return null; });
|
|
1670
2387
|
}
|
|
1671
2388
|
|
|
1672
2389
|
// Place Details photos are hosted wherever each business's site hosts
|
|
1673
2390
|
// them, so the in-chat CSP (mapAppCsp.ts) can't allow them. Inside a host,
|
|
1674
2391
|
// the page asks the server for the photo (PlacePhotoResource, by mapbox_id)
|
|
1675
|
-
// and shows it as a data: URL. A
|
|
1676
|
-
//
|
|
2392
|
+
// and shows it as a data: URL. A page with no host has no CSP of ours, so
|
|
2393
|
+
// it loads the photo URL directly.
|
|
1677
2394
|
var MAX_PANEL_PHOTOS = 10;
|
|
1678
2395
|
var EMBEDDED = window.parent !== window;
|
|
1679
2396
|
var PHOTO_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
@@ -1681,17 +2398,16 @@ ${initialDataScript}
|
|
|
1681
2398
|
|
|
1682
2399
|
function loadPanelPhoto(row, id, photoUrl) {
|
|
1683
2400
|
var show = function(src) {
|
|
1684
|
-
// A later render may have replaced the
|
|
2401
|
+
// A later render may have replaced the carousel while this loaded.
|
|
1685
2402
|
if (panelRowsById[id] !== row) return;
|
|
1686
|
-
|
|
1687
|
-
thumb.
|
|
1688
|
-
thumb.style.backgroundImage = 'url("' + src.replace(/"/g, '') + '")';
|
|
1689
|
-
row.li.appendChild(thumb);
|
|
2403
|
+
row.thumb.className = 'card-thumb';
|
|
2404
|
+
row.thumb.innerHTML = '';
|
|
2405
|
+
row.thumb.style.backgroundImage = 'url("' + src.replace(/"/g, '') + '")';
|
|
1690
2406
|
};
|
|
1691
2407
|
if (!EMBEDDED || !hostAnswered) {
|
|
1692
|
-
// No MCP host (a standalone or framed
|
|
1693
|
-
//
|
|
1694
|
-
// would leave an empty box
|
|
2408
|
+
// No MCP host (a standalone or framed page), so no CSP of ours
|
|
2409
|
+
// either. Load it first: some sites refuse hotlinked photos (403),
|
|
2410
|
+
// which would leave an empty box on the card.
|
|
1695
2411
|
if (photoUrl.indexOf('https://') !== 0 || typeof Image !== 'function') return;
|
|
1696
2412
|
var img = new Image();
|
|
1697
2413
|
img.onload = function() { show(photoUrl); };
|
|
@@ -1716,27 +2432,47 @@ ${initialDataScript}
|
|
|
1716
2432
|
detailsResult.results.forEach(function(place) {
|
|
1717
2433
|
if (!place || !place.mapbox_id) return;
|
|
1718
2434
|
var row = panelRowsById[place.mapbox_id];
|
|
1719
|
-
if (row)
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
}
|
|
1725
|
-
if (place.score && typeof place.score.popularity === 'number') {
|
|
1726
|
-
var extra = Math.round(place.score.popularity * 100) + '% popularity';
|
|
1727
|
-
row.meta.textContent = row.meta.textContent ? row.meta.textContent + ' · ' + extra : extra;
|
|
1728
|
-
}
|
|
2435
|
+
if (!row) return;
|
|
2436
|
+
var photoUrl = place.photos && place.photos[0] && place.photos[0].url;
|
|
2437
|
+
if (typeof photoUrl === 'string' && photosRequested < MAX_PANEL_PHOTOS) {
|
|
2438
|
+
photosRequested++;
|
|
2439
|
+
loadPanelPhoto(row, place.mapbox_id, photoUrl);
|
|
1729
2440
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
2441
|
+
var price = priceSigns(place.attributes && place.attributes.price_level);
|
|
2442
|
+
if (price) row.meta.textContent = cardMetaText(row.item, price);
|
|
2443
|
+
// Hours on a second line: a green "Open" or red "Closed" when we know
|
|
2444
|
+
// the place's clock. Places has no star rating, and score.popularity
|
|
2445
|
+
// isn't one, so the card shows neither.
|
|
2446
|
+
var timeZone = place.telemetry && place.telemetry.tags && place.telemetry.tags.timezone;
|
|
2447
|
+
var hours = hoursToday(place.opening_hours, new Date(), timeZone);
|
|
2448
|
+
if (hours) {
|
|
2449
|
+
var line = document.createElement('div');
|
|
2450
|
+
line.className = 'card-extra';
|
|
2451
|
+
if (hours.state === 'hours') {
|
|
2452
|
+
line.textContent = hours.text;
|
|
2453
|
+
} else {
|
|
2454
|
+
var status = document.createElement('span');
|
|
2455
|
+
status.className = hours.state === 'open' ? 'hours-open' : 'hours-closed';
|
|
2456
|
+
status.textContent = hours.state === 'open' ? 'Open' : 'Closed';
|
|
2457
|
+
line.appendChild(status);
|
|
2458
|
+
if (hours.text) {
|
|
2459
|
+
var rest = document.createElement('span');
|
|
2460
|
+
rest.textContent = ' ' + hours.text;
|
|
2461
|
+
line.appendChild(rest);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
row.body.appendChild(line);
|
|
1736
2465
|
}
|
|
1737
2466
|
});
|
|
1738
2467
|
}
|
|
1739
2468
|
|
|
2469
|
+
// A proximity the server filled in from the first result (see
|
|
2470
|
+
// searchSelfFetchProximity) only steers the re-fetch: it isn't a center
|
|
2471
|
+
// the user asked about, so no "Search center" pin and no distances.
|
|
2472
|
+
function searchCenter(params) {
|
|
2473
|
+
return params.proximityFromResults ? undefined : params.proximity;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
1740
2476
|
function selfFetchSearch(params) {
|
|
1741
2477
|
if (!TOKEN || !isSafeSearchParams(params)) {
|
|
1742
2478
|
showError('Could not fetch search results: missing token or invalid parameters.');
|
|
@@ -1750,10 +2486,7 @@ ${initialDataScript}
|
|
|
1750
2486
|
.json()
|
|
1751
2487
|
.catch(function() { return null; })
|
|
1752
2488
|
.then(function(body) {
|
|
1753
|
-
var msg =
|
|
1754
|
-
body && body.message
|
|
1755
|
-
? body.message
|
|
1756
|
-
: 'Search API error (' + res.status + ')';
|
|
2489
|
+
var msg = apiErrorText(body, 'Search API error (' + res.status + ')');
|
|
1757
2490
|
throw new Error(msg);
|
|
1758
2491
|
});
|
|
1759
2492
|
}
|
|
@@ -1771,7 +2504,7 @@ ${initialDataScript}
|
|
|
1771
2504
|
});
|
|
1772
2505
|
if (match.length > 0) fc = { type: 'FeatureCollection', features: match };
|
|
1773
2506
|
}
|
|
1774
|
-
var mini = buildSearchMiniPayload(fc, params.q, params
|
|
2507
|
+
var mini = buildSearchMiniPayload(fc, params.q, searchCenter(params));
|
|
1775
2508
|
if (!mini) {
|
|
1776
2509
|
showError('Search API returned no results.');
|
|
1777
2510
|
return;
|
|
@@ -1839,17 +2572,14 @@ ${initialDataScript}
|
|
|
1839
2572
|
.json()
|
|
1840
2573
|
.catch(function() { return null; })
|
|
1841
2574
|
.then(function(body) {
|
|
1842
|
-
var msg =
|
|
1843
|
-
body && body.message
|
|
1844
|
-
? body.message
|
|
1845
|
-
: 'Category Search API error (' + res.status + ')';
|
|
2575
|
+
var msg = apiErrorText(body, 'Category Search API error (' + res.status + ')');
|
|
1846
2576
|
throw new Error(msg);
|
|
1847
2577
|
});
|
|
1848
2578
|
}
|
|
1849
2579
|
return res.json();
|
|
1850
2580
|
})
|
|
1851
2581
|
.then(function(data) {
|
|
1852
|
-
var mini = buildSearchMiniPayload(data, params.category, params
|
|
2582
|
+
var mini = buildSearchMiniPayload(data, params.category, searchCenter(params));
|
|
1853
2583
|
if (!mini) {
|
|
1854
2584
|
showError('Category Search API returned no results.');
|
|
1855
2585
|
return;
|
|
@@ -1987,10 +2717,7 @@ ${initialDataScript}
|
|
|
1987
2717
|
.json()
|
|
1988
2718
|
.catch(function() { return null; })
|
|
1989
2719
|
.then(function(body) {
|
|
1990
|
-
var msg =
|
|
1991
|
-
body && body.message
|
|
1992
|
-
? body.message
|
|
1993
|
-
: 'Optimization API error (' + res.status + ')';
|
|
2720
|
+
var msg = apiErrorText(body, 'Optimization API error (' + res.status + ')');
|
|
1994
2721
|
throw new Error(msg);
|
|
1995
2722
|
});
|
|
1996
2723
|
}
|
|
@@ -2048,7 +2775,7 @@ ${initialDataScript}
|
|
|
2048
2775
|
|
|
2049
2776
|
// Mirrors the deleted server-side buildGroundLocationPayload (now
|
|
2050
2777
|
// client-only since the map preview self-fetches): a dark origin pin plus
|
|
2051
|
-
//
|
|
2778
|
+
// place pins for the POIs. Isochrone contours aren't drawn here either
|
|
2052
2779
|
// — the original never rendered them, only a contour-minutes summary.
|
|
2053
2780
|
function buildGroundLocationMiniPayload(place, longitude, latitude, pois) {
|
|
2054
2781
|
var markers = [
|
|
@@ -2060,7 +2787,7 @@ ${initialDataScript}
|
|
|
2060
2787
|
}
|
|
2061
2788
|
];
|
|
2062
2789
|
(pois || []).forEach(function(poi, i) {
|
|
2063
|
-
var parts = [
|
|
2790
|
+
var parts = [poi.name];
|
|
2064
2791
|
if (poi.address) parts.push(poi.address);
|
|
2065
2792
|
if (typeof poi.distance_meters === 'number') {
|
|
2066
2793
|
parts.push(Math.round(poi.distance_meters) + ' m');
|
|
@@ -2068,9 +2795,7 @@ ${initialDataScript}
|
|
|
2068
2795
|
markers.push({
|
|
2069
2796
|
id: poi.id,
|
|
2070
2797
|
coordinates: [poi.longitude, poi.latitude],
|
|
2071
|
-
style: '
|
|
2072
|
-
label: String(i + 1),
|
|
2073
|
-
color: '#f97316',
|
|
2798
|
+
style: 'place',
|
|
2074
2799
|
popup: parts.join(' — '),
|
|
2075
2800
|
name: poi.name,
|
|
2076
2801
|
category: poi.category,
|