map: restore functionality

pull/396/head
Varun Patil 2023-02-08 13:35:42 -08:00
parent bcc35d6132
commit 7d90aeacb1
11 changed files with 209 additions and 132 deletions

View File

@ -458,12 +458,13 @@ class ApiBase extends Controller
}
// Filter geological bounds
$minLat = $this->request->getParam('minLat');
$maxLat = $this->request->getParam('maxLat');
$minLng = $this->request->getParam('minLng');
$maxLng = $this->request->getParam('maxLng');
if ($minLat && $maxLat && $minLng && $maxLng) {
$transforms[] = [$this->timelineQuery, 'transformBoundFilter', $minLat, $maxLat, $minLng, $maxLng];
$bounds = $this->request->getParam('mapbounds');
if ($bounds) {
$bounds = explode(',', $bounds);
$bounds = array_map('floatval', $bounds);
if (4 === \count($bounds)) {
$transforms[] = [$this->timelineQuery, 'transformBoundFilter', $bounds];
}
}
return $transforms;

View File

@ -46,22 +46,24 @@ class LocationController extends ApiBase
// Get the folder to show
$root = null;
try {
$root = $this->getRequestRoot();
} catch (\Exception $e) {
return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
// Just check bound parameters instead of using them; they are used in transformation
$minLat = $this->request->getParam('minLat');
$maxLat = $this->request->getParam('maxLat');
$minLng = $this->request->getParam('minLng');
$maxLng = $this->request->getParam('maxLng');
// Make sure we have bounds
// $bounds = $this->request->getParam('bounds');
// if (!$bounds) {
// return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED);
// }
if (!is_numeric($minLat) || !is_numeric($maxLat) || !is_numeric($minLng) || !is_numeric($maxLng)) {
return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED);
}
// // Make sure we have 4 bounds
// $bounds = explode(',', $bounds);
// $bounds = array_map('floatval', $bounds);
// if (4 !== \count($bounds)) {
// return new JSONResponse(['message' => 'Invalid perameters'], Http::STATUS_PRECONDITION_FAILED);
// }
// Zoom level is used to determine the grid length
$zoomLevel = $this->request->getParam('zoom');

View File

@ -40,14 +40,14 @@ trait TimelineQueryFilters
$query->setMaxResults($limit);
}
public function transformBoundFilter(IQueryBuilder &$query, string $userId, string $minLat, string $maxLat, string $minLng, string $maxLng)
public function transformBoundFilter(IQueryBuilder &$query, string $userId, array $bounds)
{
$query->andWhere(
$query->expr()->andX(
$query->expr()->gte('m.lat', $query->createNamedParameter($minLat, IQueryBuilder::PARAM_STR)),
$query->expr()->lte('m.lat', $query->createNamedParameter($maxLat, IQueryBuilder::PARAM_STR)),
$query->expr()->gte('m.lon', $query->createNamedParameter($minLng, IQueryBuilder::PARAM_STR)),
$query->expr()->lte('m.lon', $query->createNamedParameter($maxLng, IQueryBuilder::PARAM_STR))
$query->expr()->gte('m.lat', $query->createNamedParameter($bounds[0], IQueryBuilder::PARAM_STR)),
$query->expr()->lte('m.lat', $query->createNamedParameter($bounds[1], IQueryBuilder::PARAM_STR)),
$query->expr()->gte('m.lon', $query->createNamedParameter($bounds[2], IQueryBuilder::PARAM_STR)),
$query->expr()->lte('m.lon', $query->createNamedParameter($bounds[3], IQueryBuilder::PARAM_STR))
)
);
}

View File

@ -30,7 +30,12 @@
</NcAppNavigation>
<NcAppContent>
<div class="outer">
<div
:class="{
outer: true,
'remove-gap': removeNavGap,
}"
>
<router-view />
</div>
</NcAppContent>
@ -144,6 +149,10 @@ export default defineComponent({
showNavigation(): boolean {
return !this.$route.name?.endsWith("-share");
},
removeNavGap(): boolean {
return this.$route.name === "locations";
},
},
watch: {
@ -330,6 +339,10 @@ export default defineComponent({
padding: 0 0 0 44px;
height: 100%;
width: 100%;
&.remove-gap {
padding: 0;
}
}
@media (max-width: 768px) {

View File

@ -0,0 +1,68 @@
<template>
<div class="container">
<div class="primary">
<component :is="primary" />
</div>
<div class="timeline">
<Timeline />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import Timeline from "./Timeline.vue";
import LocationTopMatter from "./top-matter/LocationTopMatter.vue";
export default defineComponent({
name: "SplitTimeline",
components: {
Timeline,
},
computed: {
primary() {
switch (this.$route.name) {
case "locations":
return LocationTopMatter;
default:
return "None";
}
},
},
});
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
display: flex;
overflow: hidden;
}
.primary {
width: 60%;
height: 100%;
}
.timeline {
flex: 1;
height: 100%;
padding-left: 8px;
}
@media (max-width: 768px) {
.container {
flex-direction: column;
}
.primary {
width: 100%;
height: 40%;
}
.timeline {
width: 100%;
height: 60%;
padding-left: 0;
}
}
</style>

View File

@ -5,7 +5,7 @@
:class="{ 'icon-loading': loading > 0 }"
>
<!-- Static top matter -->
<TopMatter ref="topmatter" @updateBoundary="updateBoundary" />
<TopMatter ref="topmatter" />
<!-- No content found and nothing is loading -->
<NcEmptyContent
@ -140,15 +140,7 @@ import { subscribe, unsubscribe } from "@nextcloud/event-bus";
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent";
import { getLayout } from "../services/Layout";
import {
IDay,
IFolder,
IHeadRow,
IPhoto,
IRow,
IRowType,
MapBoundary,
} from "../types";
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types";
import Folder from "./frame/Folder.vue";
import Photo from "./frame/Photo.vue";
import Tag from "./frame/Tag.vue";
@ -231,14 +223,6 @@ export default defineComponent({
selectionManager: null as InstanceType<typeof SelectionManager> & any,
/** Scroller manager component */
scrollerManager: null as InstanceType<typeof ScrollerManager> & any,
/** The boundary of the map */
mapBoundary: {
minLat: -90,
maxLat: 90,
minLng: -180,
maxLng: 180,
} as MapBoundary,
}),
mounted() {
@ -341,13 +325,16 @@ export default defineComponent({
methods: {
async routeChange(to: any, from?: any) {
if (
from?.path !== to.path ||
JSON.stringify(from.query) !== JSON.stringify(to.query)
) {
// Always do a hard refresh if the path changes
if (from?.path !== to.path) {
await this.refresh();
}
// Do a soft refresh if the query changes
else if (JSON.stringify(from.query) !== JSON.stringify(to.query)) {
await this.softRefresh();
}
// The viewer might change the route immediately again
await this.$nextTick();
@ -408,7 +395,9 @@ export default defineComponent({
},
isMobileLayout() {
return globalThis.windowInnerWidth <= 600;
return (
globalThis.windowInnerWidth <= 600 || this.$route.name === "locations"
);
},
allowBreakout() {
@ -700,20 +689,17 @@ export default defineComponent({
query.set("tag", <string>this.$route.params.name);
}
// Map Bounds
if (this.$route.name === "locations" && this.$route.query.b) {
query.set("mapbounds", <string>this.$route.query.b);
}
// Month view
if (this.isMonthView) {
query.set("monthView", "1");
query.set("reverse", "1");
}
// Geological Bounds
if (this.$route.name === "locations") {
query.set("minLat", "" + this.mapBoundary.minLat);
query.set("maxLat", "" + this.mapBoundary.maxLat);
query.set("minLng", "" + this.mapBoundary.minLng);
query.set("maxLng", "" + this.mapBoundary.maxLng);
}
return query;
},
@ -1314,11 +1300,6 @@ export default defineComponent({
this.processDay(day.dayid, newDetail);
}
},
async updateBoundary(mapBoundary: MapBoundary) {
this.mapBoundary = mapBoundary;
await this.softRefresh();
},
},
});
</script>
@ -1347,7 +1328,7 @@ export default defineComponent({
will-change: scroll-position;
contain: strict;
height: 300px;
width: calc(100% + 20px);
width: 100%;
transition: opacity 0.2s ease-in-out;
:deep .vue-recycle-scroller__slot {
@ -1365,6 +1346,7 @@ export default defineComponent({
&.empty {
opacity: 0;
transition: none;
width: 0;
}
}

View File

@ -1,11 +1,12 @@
<template>
<div class="location-top-matter">
<l-map
style="height: 100%; width: 100%; margin-right: 3.5em; z-index: 0"
:zoom="zoom"
class="map"
ref="map"
@moveend="updateMapAndTimeline"
@zoomend="updateMapAndTimeline"
:zoom="zoom"
:minZoom="2"
@moveend="refresh"
@zoomend="refresh"
>
<l-tile-layer :url="url" :attribution="attribution" />
<l-marker
@ -20,21 +21,20 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { defineComponent } from "vue";
import { LMap, LTileLayer, LMarker, LPopup } from "vue2-leaflet";
import { IMarkerCluster } from "../../types";
import { Icon } from "leaflet";
import { API } from "../../services/API";
import axios from "axios";
import Vue2LeafletMarkerCluster from "vue2-leaflet-markercluster";
import "leaflet/dist/leaflet.css";
import { IPhoto, MarkerClusters } from "../../types";
import { Icon } from "leaflet";
import axios from "axios";
import { API } from "../../services/API";
type D = Icon.Default & {
_getIconUrl?: string;
};
delete (Icon.Default.prototype as D)._getIconUrl;
const TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
const ATTRIBUTION =
'&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
Icon.Default.mergeOptions({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
@ -53,63 +53,55 @@ export default defineComponent({
},
data: () => ({
name: "locations", // add for test
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
attribution:
'&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors',
zoom: 1,
clusters: [] as MarkerClusters[],
url: TILE_URL,
attribution: ATTRIBUTION,
zoom: 2,
clusters: [] as IMarkerCluster[],
}),
watch: {
$route: function (from: any, to: any) {
this.createMatter();
},
},
mounted() {
this.createMatter();
this.updateMapAndTimeline();
},
const map = this.$refs.map as LMap;
computed: {
getPhotos() {
let photos: IPhoto[] = [];
return photos;
},
// Make sure the zoom control doesn't overlap with the navbar
map.mapObject.zoomControl.setPosition("topright");
// Initialize
this.refresh();
},
methods: {
createMatter() {
this.name = <string>this.$route.params.name || "";
},
async refresh() {
const map = this.$refs.map as LMap;
async updateMapAndTimeline() {
let map = this.$refs.map as LMap;
let boundary = map.mapObject.getBounds();
let minLat = boundary.getSouth();
let maxLat = boundary.getNorth();
let minLng = boundary.getWest();
let maxLng = boundary.getEast();
let zoomLevel = map.mapObject.getZoom().toString();
// Get boundaries of the map
const boundary = map.mapObject.getBounds();
const minLat = boundary.getSouth();
const maxLat = boundary.getNorth();
const minLon = boundary.getWest();
const maxLon = boundary.getEast();
this.$parent.$emit("updateBoundary", {
minLat: minLat,
maxLat: maxLat,
minLng: minLng,
maxLng: maxLng,
});
// Set query parameters to route if required
const s = (x: number) => x.toFixed(6);
const bounds = `${s(minLat)},${s(maxLat)},${s(minLon)},${s(maxLon)}`;
const zoom = map.mapObject.getZoom().toString();
if (this.$route.query.b === bounds && this.$route.query.z === zoom) {
return;
}
this.$router.replace({ query: { b: bounds, z: zoom } });
// Get query parameters for cluster API
// const mapWidth = maxLat - minLat;
// const mapHeight = maxLon - minLon;
let mapWidth = maxLat - minLat;
let mapHeight = maxLng - minLng;
const query = new URLSearchParams();
// Show clusters correctly while draging the map
query.set("minLat", (minLat - mapWidth).toString());
query.set("maxLat", (maxLat + mapWidth).toString());
query.set("minLng", (minLng - mapHeight).toString());
query.set("maxLng", (maxLng + mapHeight).toString());
query.set("zoom", zoomLevel);
const query = new URLSearchParams();
// query.set("minLat", (minLat - mapWidth).toString());
// query.set("maxLat", (maxLat + mapWidth).toString());
// query.set("minLon", (minLon - mapHeight).toString());
// query.set("maxLon", (maxLon + mapHeight).toString());
query.set("zoom", zoom);
// Make API call
const url = API.Q(API.CLUSTERS(), query);
const res = await axios.get(url);
this.clusters = res.data;
@ -124,8 +116,14 @@ export default defineComponent({
@import "~leaflet.markercluster/dist/MarkerCluster.Default.css";
.location-top-matter {
display: flex;
vertical-align: middle;
height: 20em;
height: 100%;
width: 100%;
}
.map {
height: 100%;
width: 100%;
margin: 0;
z-index: 0;
}
</style>

View File

@ -4,7 +4,6 @@
<TagTopMatter v-else-if="type === 2" />
<FaceTopMatter v-else-if="type === 3" />
<AlbumTopMatter v-else-if="type === 4" />
<LocationTopMatter v-else-if="type === 5" />
</div>
</template>
@ -65,8 +64,6 @@ export default defineComponent({
return this.$route.params.name
? TopMatterType.TAG
: TopMatterType.NONE;
case "locations":
return TopMatterType.LOCATION;
default:
return TopMatterType.NONE;
}

View File

@ -87,3 +87,18 @@ aside.app-sidebar {
transform: scale(1.05);
}
}
// Hide scrollbar
.recycler::-webkit-scrollbar {
display: none;
width: 0 !important;
}
.recycler {
scrollbar-width: none;
-ms-overflow-style: none;
}
// Make sure empty content is full width
[role="note"].empty-content {
width: 100%;
}

View File

@ -3,6 +3,7 @@ import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import Router from "vue-router";
import Vue from "vue";
import Timeline from "./components/Timeline.vue";
import SplitTimeline from "./components/SplitTimeline.vue";
Vue.use(Router);
@ -141,11 +142,11 @@ export default new Router({
{
path: "/locations",
component: Timeline,
component: SplitTimeline,
name: "locations",
props: (route) => ({
rootTitle: t("memories", "Locations"),
})
}),
},
],
});

View File

@ -240,14 +240,14 @@ export type ISelectionAction = {
allowPublic?: boolean;
};
export type MapBoundary = {
export type IMapBoundary = {
minLat: number;
maxLat: number;
minLng: number;
maxLng: number;
}
minLon: number;
maxLon: number;
};
export type MarkerClusters = {
export type IMarkerCluster = {
center: [number, number];
count: number;
}
};