2023-01-25 18:41:55 +00:00
< template >
2023-02-10 18:31:57 +00:00
< div
: class = " {
'map-matter' : true ,
'anim-markers' : animMarkers ,
} "
>
2023-02-09 05:55:12 +00:00
< LMap
2023-02-08 21:35:42 +00:00
class = "map"
2023-01-26 04:51:42 +00:00
ref = "map"
2023-02-09 20:02:11 +00:00
: crossOrigin = "true"
2023-02-08 21:35:42 +00:00
: zoom = "zoom"
: minZoom = "2"
@ moveend = "refresh"
@ zoomend = "refresh"
2023-02-10 19:58:54 +00:00
: options = "mapOptions"
2023-01-26 04:51:42 +00:00
>
2023-02-10 19:58:54 +00:00
< LTileLayer :url ="tileurl" :attribution ="attribution" :noWrap ="true" / >
2023-02-09 05:55:12 +00:00
< LMarker
2023-02-08 03:59:04 +00:00
v - for = "cluster in clusters"
2023-02-09 07:55:54 +00:00
: key = "cluster.id"
2023-02-08 03:59:04 +00:00
: lat - lng = "cluster.center"
2023-02-10 01:27:03 +00:00
@ click = "zoomTo(cluster)"
2023-02-08 03:59:04 +00:00
>
2023-02-10 19:42:16 +00:00
< LIcon : icon -anchor = " [ 24 , 24 ] " :className ="clusterIconClass(cluster)" >
2023-02-09 05:55:12 +00:00
< div class = "preview" >
2023-02-09 09:01:15 +00:00
< div class = "count" v-if ="cluster.count > 1" >
{ { cluster . count } }
< / div >
2023-02-10 01:41:00 +00:00
< img
: src = "clusterPreviewUrl(cluster)"
: class = " [
'thumb-important' ,
` memories-thumb- ${ cluster . preview . fileid } ` ,
] "
/ >
2023-02-09 05:55:12 +00:00
< / div >
< / LIcon >
< / LMarker >
< / LMap >
2023-01-26 04:51:42 +00:00
< / div >
2023-01-25 18:41:55 +00:00
< / template >
< script lang = "ts" >
2023-02-08 21:35:42 +00:00
import { defineComponent } from "vue" ;
2023-02-09 05:55:12 +00:00
import { LMap , LTileLayer , LMarker , LPopup , LIcon } from "vue2-leaflet" ;
2023-02-10 19:58:54 +00:00
import { latLngBounds } from "leaflet" ;
2023-02-10 01:27:03 +00:00
import { IPhoto } from "../../types" ;
2023-02-08 21:35:42 +00:00
2023-02-08 03:59:04 +00:00
import { API } from "../../services/API" ;
2023-02-10 01:27:03 +00:00
import { getPreviewUrl } from "../../services/FileUtils" ;
2023-02-09 16:29:53 +00:00
import axios from "@nextcloud/axios" ;
2023-02-10 01:27:03 +00:00
import * as utils from "../../services/Utils" ;
2023-01-25 18:41:55 +00:00
2023-02-08 21:35:42 +00:00
import "leaflet/dist/leaflet.css" ;
2023-01-25 18:41:55 +00:00
2023-02-09 17:51:26 +00:00
const OSM _TILE _URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" ;
const OSM _ATTRIBUTION =
2023-02-08 21:35:42 +00:00
'© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors' ;
2023-02-09 17:51:26 +00:00
const STAMEN _URL = ` https://stamen-tiles-{s}.a.ssl.fastly.net/terrain-background/{z}/{x}/{y}{r}.png ` ;
const STAMEN _ATTRIBUTION = ` Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>. ` ;
2023-01-25 18:41:55 +00:00
2023-02-10 19:42:16 +00:00
// CSS transition time for zooming in/out cluster animation
const CLUSTER _TRANSITION _TIME = 300 ;
2023-02-08 21:53:38 +00:00
type IMarkerCluster = {
2023-02-09 05:55:12 +00:00
id ? : number ;
2023-02-08 21:53:38 +00:00
center : [ number , number ] ;
count : number ;
2023-02-10 01:27:03 +00:00
preview ? : IPhoto ;
2023-02-10 19:42:16 +00:00
dummy ? : boolean ;
2023-02-08 21:53:38 +00:00
} ;
2023-01-25 18:41:55 +00:00
export default defineComponent ( {
2023-02-08 22:13:13 +00:00
name : "MapSplitMatter" ,
2023-01-26 04:51:42 +00:00
components : {
LMap ,
LTileLayer ,
LMarker ,
LPopup ,
2023-02-09 05:55:12 +00:00
LIcon ,
2023-01-26 04:51:42 +00:00
} ,
data : ( ) => ( {
2023-02-08 21:35:42 +00:00
zoom : 2 ,
2023-02-10 19:58:54 +00:00
mapOptions : {
maxBounds : latLngBounds ( [ - 90 , - 180 ] , [ 90 , 180 ] ) ,
maxBoundsViscosity : 0.9 ,
} ,
2023-02-08 21:35:42 +00:00
clusters : [ ] as IMarkerCluster [ ] ,
2023-02-10 18:31:57 +00:00
animMarkers : false ,
2023-02-10 20:22:08 +00:00
isDark : false ,
2023-01-26 04:51:42 +00:00
} ) ,
mounted ( ) {
2023-02-08 21:35:42 +00:00
const map = this . $refs . map as LMap ;
2023-01-25 18:41:55 +00:00
2023-02-08 21:35:42 +00:00
// Make sure the zoom control doesn't overlap with the navbar
map . mapObject . zoomControl . setPosition ( "topright" ) ;
// Initialize
this . refresh ( ) ;
2023-02-10 20:22:08 +00:00
// If currently dark mode, set isDark
const pane = document . querySelector ( ".leaflet-tile-pane" ) ;
this . isDark =
! pane || window . getComputedStyle ( pane ) ? . [ "filter" ] ? . includes ( "invert" ) ;
2023-01-26 04:51:42 +00:00
} ,
2023-01-25 18:41:55 +00:00
2023-02-09 17:51:26 +00:00
computed : {
tileurl ( ) {
2023-02-10 20:22:08 +00:00
return this . zoom >= 5 || this . isDark ? OSM _TILE _URL : STAMEN _URL ;
2023-02-09 17:51:26 +00:00
} ,
attribution ( ) {
2023-02-10 20:22:08 +00:00
return this . zoom >= 5 || this . isDark
? OSM _ATTRIBUTION
: STAMEN _ATTRIBUTION ;
2023-02-09 17:51:26 +00:00
} ,
} ,
2023-01-26 04:51:42 +00:00
methods : {
2023-02-08 21:35:42 +00:00
async refresh ( ) {
const map = this . $refs . map as LMap ;
// Get boundaries of the map
const boundary = map . mapObject . getBounds ( ) ;
2023-02-10 19:59:11 +00:00
let minLat = boundary . getSouth ( ) ;
let maxLat = boundary . getNorth ( ) ;
let minLon = boundary . getWest ( ) ;
let maxLon = boundary . getEast ( ) ;
2023-02-08 21:35:42 +00:00
// Set query parameters to route if required
const s = ( x : number ) => x . toFixed ( 6 ) ;
2023-02-10 19:59:11 +00:00
const bounds = ( ) =>
` ${ s ( minLat ) } , ${ s ( maxLat ) } , ${ s ( minLon ) } , ${ s ( maxLon ) } ` ;
2023-02-10 18:31:57 +00:00
// Zoom level
const oldZoom = this . zoom ;
2023-02-10 19:59:11 +00:00
const newZoom = Math . round ( map . mapObject . getZoom ( ) ) ;
const zoomStr = newZoom . toString ( ) ;
this . zoom = newZoom ;
2023-02-10 18:31:57 +00:00
// Check if we already have the data
2023-02-10 19:59:11 +00:00
if ( this . $route . query . b === bounds ( ) && this . $route . query . z === zoomStr ) {
2023-02-08 21:35:42 +00:00
return ;
}
2023-02-10 19:59:11 +00:00
this . $router . replace ( {
query : {
b : bounds ( ) ,
z : zoomStr ,
} ,
} ) ;
// Extend bounds by 25% beyond the map
const latDiff = Math . abs ( maxLat - minLat ) ;
const lonDiff = Math . abs ( maxLon - minLon ) ;
minLat -= latDiff * 0.25 ;
maxLat += latDiff * 0.25 ;
minLon -= lonDiff * 0.25 ;
maxLon += lonDiff * 0.25 ;
2023-02-08 21:35:42 +00:00
2023-02-08 18:52:53 +00:00
// Show clusters correctly while draging the map
2023-02-08 21:35:42 +00:00
const query = new URLSearchParams ( ) ;
2023-02-10 19:59:11 +00:00
query . set ( "bounds" , bounds ( ) ) ;
2023-02-10 18:31:57 +00:00
query . set ( "zoom" , zoomStr ) ;
2023-02-08 03:59:04 +00:00
2023-02-08 21:35:42 +00:00
// Make API call
2023-02-08 22:13:13 +00:00
const url = API . Q ( API . MAP _CLUSTERS ( ) , query ) ;
2023-02-08 03:59:04 +00:00
const res = await axios . get ( url ) ;
2023-02-10 18:31:57 +00:00
2023-02-10 19:42:16 +00:00
if ( this . zoom > oldZoom ) {
this . setClustersZoomIn ( res . data , oldZoom ) ;
} else if ( this . zoom < oldZoom ) {
this . setClustersZoomOut ( res . data ) ;
} else {
this . clusters = res . data ;
2023-02-10 18:31:57 +00:00
}
2023-02-10 19:42:16 +00:00
// Animate markers
this . animateMarkers ( ) ;
2023-01-25 18:41:55 +00:00
} ,
2023-02-09 05:55:12 +00:00
clusterPreviewUrl ( cluster : IMarkerCluster ) {
2023-02-10 02:14:12 +00:00
return getPreviewUrl ( cluster . preview , false , 256 ) ;
2023-02-09 05:55:12 +00:00
} ,
2023-02-09 07:15:48 +00:00
2023-02-10 19:42:16 +00:00
clusterIconClass ( cluster : IMarkerCluster ) {
return cluster . dummy ? "dummy" : "" ;
} ,
2023-02-10 01:27:03 +00:00
zoomTo ( cluster : IMarkerCluster ) {
// At high zoom levels, open the photo
2023-02-10 01:41:00 +00:00
if ( this . zoom >= 12 && cluster . preview ) {
2023-02-10 01:27:03 +00:00
cluster . preview . key = cluster . preview . fileid . toString ( ) ;
this . $router . push ( utils . getViewerRoute ( cluster . preview ) ) ;
return ;
}
// Zoom in
2023-02-09 07:15:48 +00:00
const map = this . $refs . map as LMap ;
2023-02-10 00:25:00 +00:00
const factor = globalThis . innerWidth >= 768 ? 2 : 1 ;
const zoom = map . mapObject . getZoom ( ) + factor ;
2023-02-10 01:27:03 +00:00
map . mapObject . setView ( cluster . center , zoom , { animate : true } ) ;
2023-02-09 07:15:48 +00:00
} ,
2023-02-10 18:31:57 +00:00
2023-02-10 19:42:16 +00:00
getGridKey ( center : [ number , number ] , zoom : number ) {
// Calcluate grid length
const clusterDensity = 1 ;
const oldGridLen = 180.0 / ( 2 * * zoom * clusterDensity ) ;
// Get map key
const latGid = Math . floor ( center [ 0 ] / oldGridLen ) ;
const lonGid = Math . floor ( center [ 1 ] / oldGridLen ) ;
return ` ${ latGid } - ${ lonGid } ` ;
} ,
getGridMap ( clusters : IMarkerCluster [ ] , zoom : number ) {
const gridMap = new Map < string , IMarkerCluster > ( ) ;
for ( const cluster of clusters ) {
const key = this . getGridKey ( cluster . center , zoom ) ;
gridMap . set ( key , cluster ) ;
}
return gridMap ;
} ,
async setClustersZoomIn ( clusters : IMarkerCluster [ ] , oldZoom : number ) {
// Create GID-map for old clusters
const oldClusters = this . getGridMap ( this . clusters , oldZoom ) ;
// Dummy clusters to animate markers
const dummyClusters : IMarkerCluster [ ] = [ ] ;
// Iterate new clusters
for ( const cluster of clusters ) {
// Check if cluster already exists
const key = this . getGridKey ( cluster . center , oldZoom ) ;
const oldCluster = oldClusters . get ( key ) ;
if ( oldCluster ) {
// Copy cluster and set location to old cluster
dummyClusters . push ( {
... cluster ,
center : oldCluster . center ,
} ) ;
} else {
// Just show it
dummyClusters . push ( cluster ) ;
}
}
// Set clusters
this . clusters = dummyClusters ;
await this . $nextTick ( ) ;
await new Promise ( ( r ) => setTimeout ( r , 0 ) ) ;
this . clusters = clusters ;
} ,
async setClustersZoomOut ( clusters : IMarkerCluster [ ] ) {
// Get GID-map for new clusters
const newClustersGid = this . getGridMap ( clusters , this . zoom ) ;
// Get ID-map for new clusters
const newClustersId = new Map < number , IMarkerCluster > ( ) ;
for ( const cluster of clusters ) {
newClustersId . set ( cluster . id , cluster ) ;
}
// Dummy clusters to animate markers
const dummyClusters : IMarkerCluster [ ] = [ ... clusters ] ;
// Iterate old clusters
for ( const oldCluster of this . clusters ) {
// Process only clusters that are not in the new clusters
const newCluster = newClustersId . get ( oldCluster . id ) ;
if ( ! newCluster ) {
// Get the new cluster at the same GID
const key = this . getGridKey ( oldCluster . center , this . zoom ) ;
const newCluster = newClustersGid . get ( key ) ;
if ( newCluster ) {
// No need to copy; it is gone anyway
oldCluster . center = newCluster . center ;
oldCluster . dummy = true ;
dummyClusters . push ( oldCluster ) ;
}
}
}
// Set clusters
this . clusters = dummyClusters ;
await new Promise ( ( r ) => setTimeout ( r , CLUSTER _TRANSITION _TIME ) ) ; // wait for animation
this . clusters = clusters ;
} ,
2023-02-10 18:31:57 +00:00
async animateMarkers ( ) {
this . animMarkers = true ;
2023-02-10 19:42:16 +00:00
await new Promise ( ( r ) => setTimeout ( r , CLUSTER _TRANSITION _TIME ) ) ; // wait for animation
2023-02-10 18:31:57 +00:00
this . animMarkers = false ;
} ,
2023-01-26 04:51:42 +00:00
} ,
2023-01-25 18:41:55 +00:00
} ) ;
< / script >
< style lang = "scss" scoped >
2023-02-08 22:13:13 +00:00
. map - matter {
2023-02-08 21:35:42 +00:00
height : 100 % ;
width : 100 % ;
}
. map {
height : 100 % ;
width : 100 % ;
margin : 0 ;
z - index : 0 ;
2023-02-10 20:22:08 +00:00
background - color : var ( -- color - background - dark ) ;
: deep . leaflet - control - attribution {
background - color : var ( -- color - background - dark ) ;
color : var ( -- color - text - light ) ;
}
: deep . leaflet - bar a {
background - color : var ( -- color - main - background ) ;
color : var ( -- color - main - text ) ;
& . leaflet - disabled {
opacity : 0.6 ;
}
}
2023-01-25 18:41:55 +00:00
}
2023-02-09 05:55:12 +00:00
. preview {
width : 48 px ;
height : 48 px ;
2023-02-09 16:25:37 +00:00
background - color : rgba ( 0 , 0 , 0 , 0.3 ) ;
2023-02-09 05:55:12 +00:00
border - radius : 5 px ;
position : relative ;
transition : transform 0.2 s ;
& : hover {
transform : scale ( 1.8 ) ;
}
img {
width : 100 % ;
height : 100 % ;
object - fit : cover ;
border - radius : 5 px ;
2023-02-09 07:15:48 +00:00
cursor : pointer ;
2023-02-09 05:55:12 +00:00
}
. count {
position : absolute ;
top : 0 ;
right : 0 ;
2023-02-10 04:13:22 +00:00
background - color : var ( -- color - primary ) ;
2023-02-09 05:55:12 +00:00
color : var ( -- color - primary - text ) ;
padding : 0 4 px ;
border - radius : 5 px ;
font - size : 0.8 em ;
}
}
< / style >
< style lang = "scss" >
2023-02-09 07:55:54 +00:00
. leaflet - marker - icon {
2023-02-10 18:31:57 +00:00
. anim - markers & {
2023-02-10 19:42:16 +00:00
transition : transform 0.3 s ease ;
2023-02-10 18:31:57 +00:00
}
2023-02-09 07:55:54 +00:00
2023-02-10 19:42:16 +00:00
& . dummy {
z - index : - 100000 ! important ;
}
& : hover {
z - index : 100000 ! important ;
}
2023-02-09 05:55:12 +00:00
}
2023-02-09 07:55:54 +00:00
2023-02-09 17:17:24 +00:00
// Dark mode
$darkFilter : invert ( 1 ) grayscale ( 1 ) brightness ( 1.3 ) contrast ( 1.3 ) ;
. leaflet - tile - pane {
body [ data - theme - dark ] & ,
body [ data - theme - dark - highcontrast ] & {
filter : $darkFilter ;
}
@ media ( prefers - color - scheme : dark ) {
body [ data - theme - default ] & {
filter : $darkFilter ;
}
}
}
2023-01-26 04:51:42 +00:00
< / style >