1+ <!-- eslint-disable vue/multi-word-component-names -->
2+ <script setup lang="ts">
3+ import { useAppStore , useLayerStore , useMapStore } from " @/store" ;
4+ import { useMapCompareStore } from " @/store/compare" ;
5+ import { computed , onMounted , Ref , ref , watch } from " vue" ;
6+ import { ToggleCompare } from " vue-maplibre-compare" ;
7+ import { oauthClient } from " @/api/auth" ;
8+ import ' vue-maplibre-compare/dist/vue-maplibre-compare.css'
9+ import { addProtocol , AttributionControl , Popup } from " maplibre-gl" ;
10+ import type { StyleSpecification , Map , ResourceType } from " maplibre-gl" ;
11+ import { baseURL } from " @/api/auth" ;
12+ import { useTheme } from ' vuetify' ;
13+ import { Protocol } from " pmtiles" ;
14+ import { THEMES } from " @/themes" ;
15+
16+ const ATTRIBUTION = [
17+ " <a target='_blank' href='https://maplibre.org/'>© MapLibre</a>" ,
18+ " <span> | </span>" ,
19+ " <a target='_blank' href='https://www.openstreetmap.org/copyright'>© OpenStreetMap</a>" ,
20+ ];
21+
22+ addProtocol (" pmtiles" , new Protocol ().tile );
23+
24+ const appStore = useAppStore ();
25+ const mapStore = useMapStore ();
26+ const compareStore = useMapCompareStore ();
27+ const layerStore = useLayerStore ();
28+
29+ // MapLibre refs
30+ const tooltip = ref <HTMLElement >();
31+ const attributionControl = new AttributionControl ({
32+ compact: true ,
33+ customAttribution: ATTRIBUTION ,
34+ });
35+ attributionControl .onAdd = (map : Map ): HTMLElement => {
36+ attributionControl ._map = map ;
37+ const container = document .createElement (" div" );
38+ container .innerHTML = ATTRIBUTION .join (" " );
39+ attributionControl ._container = container ;
40+ setAttributionControlStyle ();
41+ return container ;
42+ };
43+
44+ function setAttributionControlStyle() {
45+ const container = attributionControl ._container ;
46+ container .style .padding = " 3px 8px" ;
47+ container .style .marginRight = " 5px" ;
48+ container .style .borderRadius = " 15px" ;
49+ container .style .position = " relative" ;
50+ container .style .right = appStore .openSidebars .includes (" right" )
51+ ? " 360px"
52+ : " 0px" ;
53+ container .style .background = appStore .theme === " light" ? " white" : " black" ;
54+ container .childNodes .forEach ((child ) => {
55+ const childElement = child as HTMLElement ;
56+ childElement .style .color = appStore .theme === " light" ? " black" : " white" ;
57+ });
58+ }
59+
60+ const handleMapReady = (newMap : Map ) => {
61+ console .log (" Compare maps ready" );
62+ newMap .addControl (attributionControl );
63+ newMap .on (' error' , (response ) => {
64+ // AbortErrors are raised when updating style of raster layers; ignore these
65+ if (response .error .message !== ' AbortError' ) console .error (response .error )
66+ });
67+
68+ /**
69+ * This is called on every click, and technically hides the tooltip on every click.
70+ * However, if a feature layer is clicked, that event is fired after this one, and the
71+ * tooltip is re-enabled and rendered with the desired contents. The net result is that
72+ * this only has a real effect when the base map is clicked, as that means that no other
73+ * feature layer can "catch" the event, and the tooltip stays hidden.
74+ */
75+ newMap .on (" click" , () => {mapStore .clickedFeature = undefined });
76+ mapStore .map = newMap ;
77+ mapStore .setMapCenter (undefined , true );
78+ createMapControls ();
79+ newMap .once (' idle' , () => {
80+ // layerStore.updateLayersShown();
81+ });
82+
83+ }
84+
85+
86+ function createMapControls() {
87+ if (! mapStore .map || ! tooltip .value ) {
88+ throw new Error (" Map or refs not initialized!" );
89+ }
90+
91+ // Add tooltip overlay
92+ const popup = new Popup ({
93+ anchor: " bottom-left" ,
94+ closeOnClick: false ,
95+ maxWidth: " none" ,
96+ closeButton: true ,
97+ });
98+
99+ // Link overlay ref to dom, allowing for modification elsewhere
100+ popup .setDOMContent (tooltip .value );
101+
102+ // Set store value
103+ mapStore .tooltipOverlay = popup ;
104+ }
105+
106+
107+ watch (() => appStore .theme , () => {
108+ if (! mapStore .map ) return ;
109+ const map = mapStore .getMap ();
110+ map .setStyle (THEMES [appStore .theme ].mapStyle );
111+ setAttributionControlStyle ();
112+ // layerStore.updateLayersShown();
113+ });
114+
115+ watch (() => appStore .openSidebars , () => {
116+ setAttributionControlStyle ();
117+ });
118+
119+ const transformRequest = (url : string , _resourceType ? : ResourceType ) => {
120+ // Only add auth headers to our own tile requests
121+ if (url .startsWith (baseURL )) {
122+ return {
123+ url ,
124+ headers: oauthClient ?.authHeaders ,
125+ };
126+ }
127+ return { url };
128+ }
129+
130+ const computedCompare = computed (() => compareStore .isComparing );
131+ const mapStats = computed (() => compareStore .mapStats );
132+ const mapStyleA: Ref <StyleSpecification > = ref (THEMES [appStore .theme ].mapStyle );
133+ watch (computedCompare , (newVal ) => {
134+ if (! newVal && mapStore .map ) {
135+ mapStore .getMap ()?.jumpTo ({
136+ center: mapStats .value ?.center ,
137+ zoom: mapStats .value ?.zoom ,
138+ bearing: mapStats .value ?.bearing ,
139+ pitch: mapStats .value ?.pitch ,
140+ });
141+ } else if (newVal ) {
142+ mapStyleA .value = mapStore .getMap ()! .getStyle ();
143+ }
144+ });
145+
146+ watch (() => compareStore .mapAStyle , (newStyle ) => {
147+ if (compareStore .isComparing && mapStore .map ) {
148+ mapStyleA .value = newStyle ;
149+ }
150+ }, { deep: true });
151+
152+
153+
154+ const mapStyleB = computed (() => compareStore .mapBStyle );
155+ const mapLayersA = computed (() => compareStore .mapLayersA );
156+ const mapLayersB = computed (() => compareStore .mapLayersB );
157+
158+ const swiperColor = computed (() => {
159+ const theme = useTheme ();
160+ return theme .global .current .value .colors .primary
161+ });
162+ </script >
163+
164+ <template >
165+ <div >
166+ <ToggleCompare
167+ :map-style-a =" mapStyleA"
168+ :map-style-b =" mapStyleB"
169+ :map-layers-a =" mapLayersA"
170+ :map-layers-b =" mapLayersB"
171+ :compare-enabled =" compareStore.isComparing"
172+ :camera =" {
173+ center: mapStats.center,
174+ zoom: mapStats.zoom,
175+ }"
176+ :transform-request =" transformRequest"
177+ :swiper-options =" {
178+ darkMode: appStore.theme !== 'dark',
179+ orientation: compareStore.orientation,
180+ grabThickness: 20,
181+ lineColor: swiperColor,
182+ handleColor: swiperColor
183+ }"
184+ layer-order =" bottommost"
185+ @panend =" compareStore.updateMapStats($event)"
186+ @zoomend =" compareStore.updateMapStats($event)"
187+ @pitchend =" compareStore.updateMapStats($event)"
188+ @rotateend =" compareStore.updateMapStats($event)"
189+ @map-ready =" handleMapReady"
190+ class =" map"
191+ />
192+
193+ <div id =" map-tooltip" ref =" tooltip" class =" tooltip pa-0" >
194+ <MapTooltip />
195+ </div >
196+ </div >
197+ </template >
198+
199+ <style scoped>
200+ .map {
201+ height : 100% ;
202+ width : 100% ;
203+ position : relative ;
204+ }
205+
206+ @keyframes spinner {
207+ to {
208+ transform : rotate (360deg );
209+ }
210+ }
211+
212+ .spinner :after {
213+ content : " " ;
214+ box-sizing : border-box ;
215+ position : absolute ;
216+ top : 50% ;
217+ left : 50% ;
218+ width : 40px ;
219+ height : 40px ;
220+ margin-top : -20px ;
221+ margin-left : -20px ;
222+ border-radius : 50% ;
223+ border : 5px solid rgba (180 , 180 , 180 , 0.6 );
224+ border-top-color : rgba (0 , 0 , 0 , 0.6 );
225+ animation : spinner 0.6s linear infinite ;
226+ }
227+
228+ .tooltip {
229+ border-radius : 5px ;
230+ padding : 10px 20px ;
231+ word-break : break-word ;
232+ text-wrap : wrap ;
233+ width : fit-content ;
234+ min-width : 50px ;
235+ max-width : 350px ;
236+ }
237+
238+ .base-layer-control {
239+ float : right ;
240+ position : absolute ;
241+ top : 2% ;
242+ right : 2% ;
243+ z-index : 2 ;
244+ }
245+ </style >
0 commit comments