-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: kvmap ported to kv components library (#286)
* feat: kvmap ported to kv components library * fix: utilities removed * fix: lock file removed * feat: vue meta package added
- Loading branch information
1 parent
cec2949
commit 373e53f
Showing
5 changed files
with
464 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,370 @@ | ||
<template> | ||
<div | ||
class="tw-relative tw-block tw-w-full" | ||
:style="mapDimensions" | ||
> | ||
<div | ||
:id="`kv-map-holder-${mapId}`" | ||
:ref="refString" | ||
class="tw-w-full tw-h-full tw-bg-black" | ||
:style="{ position: 'absolute' }" | ||
></div> | ||
</div> | ||
</template> | ||
|
||
<script> | ||
export default { | ||
name: 'KvMap', | ||
metaInfo() { | ||
return { | ||
script: [].concat(!this.hasWebGL ? [ | ||
// leaflet - uses raster tiles for additional browser coverage | ||
{ | ||
vmid: `leafletjs${this.mapId}`, | ||
src: 'https://unpkg.com/[email protected]/dist/leaflet.js', | ||
async: true, | ||
defer: true, | ||
}, | ||
] : []).concat(this.hasWebGL ? [ | ||
// maplibregl - uses vector tiles and webgl for rendering | ||
{ | ||
vmid: `maplibregljs${this.mapId}`, | ||
src: 'https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.js', | ||
async: true, | ||
defer: true, | ||
}, | ||
] : []), | ||
link: [ | ||
].concat(!this.hasWebGL ? [ | ||
// leaflet - uses raster tiles for additional browser coverage | ||
{ | ||
vmid: `leafletcss${this.mapId}`, | ||
rel: 'stylesheet', | ||
href: 'https://unpkg.com/[email protected]/dist/leaflet.css', | ||
}, | ||
] : []).concat(this.hasWebGL ? [ | ||
// maplibregl - uses vector tiles and webgl for rendering | ||
{ | ||
vmid: `maplibreglcss${this.mapId}`, | ||
rel: 'stylesheet', | ||
href: 'https://unpkg.com/maplibre-gl@latest/dist/maplibre-gl.css', | ||
}, | ||
] : []), | ||
}; | ||
}, | ||
props: { | ||
/** | ||
* Aspect Ration for computed map dimensions | ||
* We'll divide the container width by this to determine the height | ||
*/ | ||
aspectRatio: { | ||
type: Number, | ||
default: 1, | ||
}, | ||
/** | ||
* Control how quickly the autoZoom occurs | ||
*/ | ||
autoZoomDelay: { | ||
type: Number, | ||
default: 1500, | ||
}, | ||
/** | ||
* Set the height to override aspect ratio driven and/or default dimensions | ||
*/ | ||
height: { | ||
type: Number, | ||
default: null, | ||
}, | ||
/** | ||
* Setting this initialZoom will zoom the map from initialZoom to zoom when the map enters the viewport | ||
*/ | ||
initialZoom: { | ||
type: Number, | ||
default: null, | ||
}, | ||
/** | ||
* Set the center point latitude | ||
*/ | ||
lat: { | ||
type: Number, | ||
default: null, | ||
}, | ||
/** | ||
* Set the center point longitude | ||
*/ | ||
long: { | ||
type: Number, | ||
default: null, | ||
}, | ||
/** | ||
* Set this if there are more than one map on the page | ||
*/ | ||
mapId: { | ||
type: Number, | ||
default: 0, | ||
}, | ||
/** | ||
* Force use of Leaflet | ||
*/ | ||
useLeaflet: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
/** | ||
* Set the width to override aspect ratio driven and/or default dimensions | ||
*/ | ||
width: { | ||
type: Number, | ||
default: null, | ||
}, | ||
/** | ||
* Default zoom level | ||
*/ | ||
zoomLevel: { | ||
type: Number, | ||
default: 4, | ||
}, | ||
}, | ||
data() { | ||
return { | ||
hasWebGL: false, | ||
leafletReady: false, | ||
mapInstance: null, | ||
mapLibreReady: false, | ||
mapLoaded: false, | ||
zoomActive: false, | ||
}; | ||
}, | ||
computed: { | ||
mapDimensions() { | ||
// Use container to derive height based on aspect ration + width | ||
const container = this.$el?.getBoundingClientRect(); | ||
const height = container ? `${container.width / this.aspectRatio}px` : '300px'; | ||
const width = container ? `${container.width}px` : '100%'; | ||
// Override values if deliberate height or width are provided | ||
return { | ||
height: this.height ? `${this.height}px` : height, | ||
width: this.width ? `${this.width}px` : width, | ||
paddingBottom: this.height ? `${this.height}px` : `${100 / this.aspectRatio}%`, | ||
}; | ||
}, | ||
refString() { | ||
return `mapholder${this.mapId}`; | ||
}, | ||
}, | ||
watch: { | ||
lat(next, prev) { | ||
if (prev === null && this.long && !this.mapLibreReady && !this.leafletReady) { | ||
this.initializeMap(); | ||
} | ||
}, | ||
long(next, prev) { | ||
if (prev === null && this.lat && !this.mapLibreReady && !this.leafletReady) { | ||
this.initializeMap(); | ||
} | ||
}, | ||
}, | ||
mounted() { | ||
if (!this.mapLibreReady && !this.leafletReady) { | ||
this.initializeMap(); | ||
} | ||
}, | ||
beforeDestroy() { | ||
if (this.mapInstance) { | ||
if (!this.hasWebGL && !this.leafletReady) { | ||
// turn off the leaflet instance | ||
this.mapInstance.off(); | ||
} | ||
// remove either leaflet or maplibregl | ||
this.mapInstance.remove(); | ||
} | ||
this.destroyWrapperObserver(); | ||
}, | ||
methods: { | ||
activateZoom(zoomOut = false) { | ||
const { mapInstance, hasWebGL, mapLibreReady } = this; | ||
const currentZoomLevel = mapInstance.getZoom(); | ||
// exit if already zoomed in (getZoom() works for both leaflet + maplibregl) | ||
if ((!zoomOut && currentZoomLevel === this.zoomLevel) | ||
|| (zoomOut && currentZoomLevel === this.initialZoom)) return false; | ||
this.zoomActive = true; | ||
// establish delayed zoom duration | ||
const timedZoom = window.setTimeout(() => { | ||
if (hasWebGL && mapLibreReady) { | ||
// maplibregl specific zoom method | ||
mapInstance.zoomTo( | ||
zoomOut ? this.initialZoom : this.zoomLevel, | ||
{ duration: 1200 }, | ||
); | ||
} else { | ||
// leaflet specific zoom method | ||
mapInstance.setZoom(zoomOut ? this.initialZoom : this.zoomLevel); | ||
} | ||
clearTimeout(timedZoom); | ||
this.zoomActive = false; | ||
}, this.autoZoomDelay); | ||
}, | ||
createWrapperObserver() { | ||
// Watch for the wrapper element moving in and out of the viewport | ||
this.wrapperObserver = this.createIntersectionObserver({ | ||
targets: [this.$refs?.[this.refString]], | ||
callback: (entries) => { | ||
entries.forEach((entry) => { | ||
if (entry.target === this.$refs?.[this.refString] && !this.zoomActive) { | ||
if (entry.intersectionRatio > 0) { | ||
this.activateZoom(); | ||
} | ||
} | ||
}); | ||
}, | ||
}); | ||
}, | ||
destroyWrapperObserver() { | ||
if (this.wrapperObserver) { | ||
this.wrapperObserver.disconnect(); | ||
} | ||
}, | ||
checkWebGL() { | ||
// exit and use leaflet if specified or document isn't present | ||
if (this.useLeaflet || typeof document === 'undefined') return false; | ||
// via. https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/By_example/Detect_WebGL | ||
// Create canvas element. The canvas is not added to the document itself, | ||
// so it is never displayed in the browser window. | ||
const canvas = document.createElement('canvas'); | ||
// Get WebGLRenderingContext from canvas element. | ||
const gl = canvas.getContext('webgl') | ||
|| canvas.getContext('experimental-webgl'); | ||
// Report the result. | ||
if (gl && gl instanceof WebGLRenderingContext) { | ||
this.hasWebGL = true; | ||
return true; | ||
} | ||
return false; | ||
}, | ||
initializeMap() { | ||
/** | ||
* This initial checkWebGL() call kicks off the vue-meta asset inclusion | ||
* We then start polling for the readiness of our selected map library and initialize it once ready | ||
*/ | ||
if (this.checkWebGL()) { | ||
this.testDelayedGlobalLibrary('maplibregl').then((response) => { | ||
if (response.loaded && !this.mapLoaded && !this.useLeaflet && this.lat && this.long) { | ||
this.initializeMapLibre(); | ||
this.mapLibreReady = true; | ||
} | ||
}); | ||
} else { | ||
this.testDelayedGlobalLibrary('L').then((leafletTest) => { | ||
if (leafletTest.loaded && !this.mapLoaded && this.lat && this.long) { | ||
this.initializeLeaflet(); | ||
this.leafletReady = true; | ||
} | ||
}); | ||
} | ||
}, | ||
initializeLeaflet() { | ||
/* eslint-disable no-undef, max-len */ | ||
// Initialize primary mapInstance | ||
this.mapInstance = L.map(`kv-map-holder-${this.mapId}`, { | ||
center: [this.lat, this.long], | ||
zoom: this.initialZoom || this.zoomLevel, | ||
// todo make props for the following options | ||
dragging: false, | ||
zoomControl: false, | ||
animate: true, | ||
scrollWheelZoom: false, | ||
doubleClickZoom: false, | ||
attributionControl: false, | ||
}); | ||
/* eslint-disable quotes */ | ||
// Add our tileset to the mapInstance | ||
L.tileLayer('https://api.maptiler.com/maps/bright/{z}/{x}/{y}.png?key=n1Mz5ziX3k6JfdjFe7mx', { | ||
tileSize: 512, | ||
zoomOffset: -1, | ||
minZoom: 1, | ||
crossOrigin: true, | ||
}).addTo(this.mapInstance); | ||
/* eslint-enable quotes */ | ||
/* eslint-enable no-undef, max-len */ | ||
// signify map has loaded | ||
this.mapLoaded = true; | ||
// only activate autoZoom if we have an initialZoom set | ||
if (this.initialZoom !== null) { | ||
this.createWrapperObserver(); | ||
} | ||
}, | ||
initializeMapLibre() { | ||
// Initialize primary mapInstance | ||
// eslint-disable-next-line no-undef | ||
this.mapInstance = new maplibregl.Map({ | ||
container: `kv-map-holder-${this.mapId}`, | ||
style: 'https://api.maptiler.com/maps/bright/style.json?key=n1Mz5ziX3k6JfdjFe7mx', | ||
center: [this.long, this.lat], | ||
zoom: this.initialZoom || this.zoomLevel, | ||
attributionControl: false, | ||
dragPan: false, | ||
scrollZoom: false, | ||
doubleClickZoom: false, | ||
dragRotate: false, | ||
}); | ||
// signify map has loaded | ||
this.mapLoaded = true; | ||
// only activate autoZoom if we have an initialZoom set | ||
if (this.initialZoom !== null) { | ||
this.createWrapperObserver(); | ||
} | ||
}, | ||
checkIntersectionObserverSupport() { | ||
if (typeof window === 'undefined' | ||
|| !('IntersectionObserver' in window) | ||
|| !('IntersectionObserverEntry' in window) | ||
|| !('intersectionRatio' in window.IntersectionObserverEntry.prototype)) { | ||
return false; | ||
} | ||
return true; | ||
}, | ||
createIntersectionObserver({ callback, options, targets } = {}) { | ||
if (this.checkIntersectionObserverSupport()) { | ||
const observer = new IntersectionObserver(callback, options); | ||
targets.forEach((target) => observer.observe(target)); | ||
return observer; | ||
} | ||
}, | ||
testDelayedGlobalLibrary(library, timeout = 3000) { | ||
// return a promise | ||
return new Promise((resolve, reject) => { | ||
if (typeof window === 'undefined') { | ||
reject(new Error('window object not available')); | ||
} | ||
// establish timeout to limit time until promise resolution | ||
let readyStateTimeout; | ||
// establish interval to check for library presence | ||
const readyStateInterval = window.setInterval(() => { | ||
// determine if library is present on window | ||
if (typeof window[library] !== 'undefined') { | ||
// cleanup timers | ||
clearInterval(readyStateInterval); | ||
clearTimeout(readyStateTimeout); | ||
// resolve the promise | ||
resolve({ loaded: true }); | ||
} | ||
}, 100); | ||
// activate timeout | ||
readyStateTimeout = window.setTimeout(() => { | ||
// clean up interval and timeout | ||
clearInterval(readyStateInterval); | ||
clearTimeout(readyStateTimeout); | ||
// resolve the promise | ||
resolve({ loaded: false }); | ||
}, timeout); | ||
}); | ||
}, | ||
}, | ||
}; | ||
</script> |
Oops, something went wrong.