Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
405d56237e
commit
a92653d4f2
17 changed files with 470 additions and 214 deletions
|
@ -263,15 +263,15 @@ export default [
|
|||
meta: { title: $gettext("Places"), auth: true },
|
||||
},
|
||||
{
|
||||
name: "places_query",
|
||||
path: "/places/:q",
|
||||
name: "places_scope",
|
||||
path: "/places/:s",
|
||||
component: Places,
|
||||
meta: { title: $gettext("Places"), auth: true },
|
||||
},
|
||||
{
|
||||
name: "places_scope",
|
||||
path: "/places/:s/:q",
|
||||
component: Places,
|
||||
name: "places_location",
|
||||
path: "/location",
|
||||
component: Photos,
|
||||
meta: { title: $gettext("Places"), auth: true },
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,55 +1,68 @@
|
|||
<template>
|
||||
<v-form ref="form" lazy-validation
|
||||
dense autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8"
|
||||
:class="{'sticky': sticky}"
|
||||
:class="{'embedded': embedded}"
|
||||
@submit.prevent="updateQuery()">
|
||||
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" class="page-toolbar" color="secondary">
|
||||
<v-text-field :value="filter.q"
|
||||
class="input-search background-inherit elevation-0"
|
||||
solo hide-details clearable overflow single-line
|
||||
validate-on-blur
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Search')"
|
||||
prepend-inner-icon="search"
|
||||
color="secondary-dark"
|
||||
@change="(v) => {updateFilter({'q': v})}"
|
||||
@keyup.enter.native="(e) => updateQuery({'q': e.target.value})"
|
||||
@click:clear="() => {updateQuery({'q': ''})}"
|
||||
></v-text-field>
|
||||
<v-toolbar flat :dense="$vuetify.breakpoint.smAndDown" :height="embedded ? 45 : undefined"
|
||||
class="page-toolbar" color="secondary">
|
||||
<template v-if="!embedded">
|
||||
<v-text-field :value="filter.q"
|
||||
class="input-search background-inherit elevation-0"
|
||||
solo hide-details clearable overflow single-line validate-on-blur
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
browser-autocomplete="off"
|
||||
:label="$gettext('Search')"
|
||||
prepend-inner-icon="search"
|
||||
color="secondary-dark"
|
||||
@change="(v) => {updateFilter({'q': v})}"
|
||||
@keyup.enter.native="(e) => updateQuery({'q': e.target.value})"
|
||||
@click:clear="() => {updateQuery({'q': ''})}"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="filter.latlng" icon class="action-clear-location" :title="$gettext('Clear Location')" @click.stop="clearLocation()">
|
||||
<v-icon>location_off</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="settings.view === 'cards'" icon :title="$gettext('Toggle View')" @click.stop="setView('list')">
|
||||
<v-icon>view_list</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else-if="settings.view === 'list'" icon :title="$gettext('Toggle View')" @click.stop="setView('mosaic')">
|
||||
<v-icon>view_comfy</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else icon :title="$gettext('Toggle View')" @click.stop="setView('cards')">
|
||||
<v-icon>view_column</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon class="hidden-xs-only action-reload" :title="$gettext('Reload')" @click.stop="refresh()">
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="canDelete && context === 'archive' && config.count.archived > 0" icon class="hidden-sm-and-down action-sweep"
|
||||
:title="$gettext('Delete')" @click.stop="sweepArchive()">
|
||||
<v-icon>delete_sweep</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else-if="canUpload" icon class="hidden-sm-and-down action-upload"
|
||||
:title="$gettext('Upload')" @click.stop="showUpload()">
|
||||
<v-icon>cloud_upload</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="settings.view === 'cards'" icon :title="$gettext('Toggle View')" @click.stop="setView('list')">
|
||||
<v-icon>view_list</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else-if="settings.view === 'list'" icon :title="$gettext('Toggle View')"
|
||||
@click.stop="setView('mosaic')">
|
||||
<v-icon>view_comfy</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else icon :title="$gettext('Toggle View')" @click.stop="setView('cards')">
|
||||
<v-icon>view_column</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon class="p-expand-search" :title="$gettext('Expand Search')"
|
||||
@click.stop="searchExpanded = !searchExpanded">
|
||||
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="canDelete && context === 'archive' && config.count.archived > 0" icon
|
||||
class="hidden-sm-and-down action-sweep"
|
||||
:title="$gettext('Delete')" @click.stop="sweepArchive()">
|
||||
<v-icon>delete_sweep</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-else-if="canUpload" icon class="hidden-sm-and-down action-upload"
|
||||
:title="$gettext('Upload')" @click.stop="showUpload()">
|
||||
<v-icon>cloud_upload</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn v-if="onClose !== undefined" icon class="action-close" @click.stop="onClose">
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon class="p-expand-search" :title="$gettext('Expand Search')"
|
||||
@click.stop="searchExpanded = !searchExpanded">
|
||||
<v-icon>{{ searchExpanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn v-if="canAccessLibrary" icon class="action-open-tab" @click.stop="openInTab">
|
||||
<v-icon size="19">open_in_new</v-icon>
|
||||
</v-btn>
|
||||
<v-btn v-if="onClose !== undefined" icon class="action-close" @click.stop="onClose">
|
||||
<v-icon>close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-toolbar>
|
||||
|
||||
<v-card v-show="searchExpanded"
|
||||
|
@ -197,29 +210,39 @@ export default {
|
|||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
staticFilter: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
updateFilter: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
updateQuery: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
refresh: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
default: () => {
|
||||
},
|
||||
},
|
||||
onClose: {
|
||||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
sticky: {
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
@ -232,8 +255,9 @@ export default {
|
|||
isFullScreen: !!document.fullscreenElement,
|
||||
config: this.$config.values,
|
||||
readonly: readonly,
|
||||
canUpload: !readonly && this.$config.allow("files", "upload") && features.upload,
|
||||
canDelete: !readonly && this.$config.allow("photos", "delete") && features.delete,
|
||||
canUpload: !readonly && !this.embedded && this.$config.allow("files", "upload") && features.upload,
|
||||
canDelete: !readonly && !this.embedded && this.$config.allow("photos", "delete") && features.delete,
|
||||
canAccessLibrary: this.$config.allow("photos", "access_library"),
|
||||
searchExpanded: false,
|
||||
all: {
|
||||
countries: [{ID: "", Name: this.$gettext("All Countries")}],
|
||||
|
@ -306,6 +330,15 @@ export default {
|
|||
|
||||
this.dialog.delete = true;
|
||||
},
|
||||
clearLocation() {
|
||||
this.$router.push({ name: "browse" });
|
||||
},
|
||||
openInTab() {
|
||||
const url = this.$router.resolve({name: 'places_location', query: this.staticFilter}).href;
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
},
|
||||
batchDelete() {
|
||||
if (!this.canDelete) {
|
||||
return;
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
#photoprism #map .marker {
|
||||
box-shadow: 0px 3px 1px -2px rgba(0, 0, 0, 0.2), 0px 2px 2px 0px rgba(0, 0, 0, 0.14), 0px 1px 5px 0px rgba(0, 0, 0, 0.12) !important;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
background-color: rgba(23, 23, 23, 0.23);
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
|
@ -33,7 +33,7 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 1px;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff99;
|
||||
/* background-color: #ffffff99; */
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
@ -97,4 +97,52 @@
|
|||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 70%;
|
||||
}
|
||||
|
||||
#photoprism .cluster-control {
|
||||
background: #2f3031;
|
||||
position: absolute;
|
||||
top: auto;
|
||||
z-index: 2;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
left: 4px;
|
||||
right: 4px;
|
||||
border-top-left-radius: 16px;
|
||||
border-top-right-radius: 16px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;
|
||||
transition: all 1450ms cubic-bezier(0.32,1,0.23,1) 0ms;
|
||||
}
|
||||
|
||||
#photoprism .cluster-control-container {
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
#photoprism .cluster-control .p-photos {
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media only screen and (min-height: 1500px) {
|
||||
#photoprism .cluster-control-container {
|
||||
min-height: 45vh;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: portrait) {
|
||||
#photoprism .cluster-control {
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
#photoprism .cluster-control-container {
|
||||
min-height: 70vh;
|
||||
}
|
||||
}
|
|
@ -122,7 +122,7 @@ nav .v-list__tile__title.title {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
#photoprism .p-photo-toolbar.sticky {
|
||||
#photoprism .p-photo-toolbar.embedded {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
|
|
|
@ -196,9 +196,9 @@ export default {
|
|||
}
|
||||
|
||||
if (this.canAccessLibrary && photo.CellID && photo.CellID !== "zz") {
|
||||
this.$router.push({name: "places_query", params: {q: photo.CellID}});
|
||||
this.$router.push({name: "places", query: {q: photo.CellID}});
|
||||
} else if (this.uid) {
|
||||
this.$router.push({name: "places_scope", params: {s: this.uid, q: photo.CellID}});
|
||||
this.$router.push({name: "places_scope", params: {s: this.uid}, query: {q: photo.CellID}});
|
||||
}
|
||||
},
|
||||
editPhoto(index) {
|
||||
|
|
|
@ -6,12 +6,13 @@
|
|||
<p-photo-toolbar
|
||||
:context="context"
|
||||
:filter="filter"
|
||||
:static-filter="staticFilter"
|
||||
:settings="settings"
|
||||
:refresh="refresh"
|
||||
:update-filter="updateFilter"
|
||||
:update-query="updateQuery"
|
||||
:on-close="onClose"
|
||||
:sticky="stickyToolbar"
|
||||
:embedded="embedded"
|
||||
/>
|
||||
|
||||
<v-container v-if="loading" fluid class="pa-4">
|
||||
|
@ -70,7 +71,7 @@ export default {
|
|||
type: Function,
|
||||
default: undefined,
|
||||
},
|
||||
stickyToolbar: {
|
||||
embedded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
@ -87,12 +88,14 @@ export default {
|
|||
const month = query['month'] ? parseInt(query['month']) : 0;
|
||||
const color = query['color'] ? query['color'] : '';
|
||||
const label = query['label'] ? query['label'] : '';
|
||||
const latlng = query['latlng'] ? query['latlng'] : '';
|
||||
const view = this.viewType();
|
||||
const filter = {
|
||||
country: country,
|
||||
camera: camera,
|
||||
lens: lens,
|
||||
label: label,
|
||||
latlng: latlng,
|
||||
year: year,
|
||||
month: month,
|
||||
color: color,
|
||||
|
@ -192,6 +195,7 @@ export default {
|
|||
this.filter.month = query['month'] ? parseInt(query['month']) : 0;
|
||||
this.filter.color = query['color'] ? query['color'] : '';
|
||||
this.filter.label = query['label'] ? query['label'] : '';
|
||||
this.filter.latlng = query['latlng'] ? query['latlng'] : '';
|
||||
this.filter.order = this.sortOrder();
|
||||
|
||||
this.settings.view = this.viewType();
|
||||
|
@ -253,8 +257,12 @@ export default {
|
|||
window.localStorage.setItem("photos_offset", offset);
|
||||
},
|
||||
viewType() {
|
||||
let queryParam = this.$route.query['view'] ? this.$route.query['view'] : "";
|
||||
let storedType = window.localStorage.getItem("photos_view");
|
||||
if (this.embedded) {
|
||||
return 'mosaic';
|
||||
}
|
||||
|
||||
let queryParam = this.$route.query['view'] ? this.$route.query['view'] : '';
|
||||
let storedType = window.localStorage.getItem('photos_view');
|
||||
|
||||
if (queryParam) {
|
||||
window.localStorage.setItem("photos_view", queryParam);
|
||||
|
@ -268,17 +276,21 @@ export default {
|
|||
return 'cards';
|
||||
},
|
||||
sortOrder() {
|
||||
let queryParam = this.$route.query["order"];
|
||||
let storedType = window.localStorage.getItem("photos_order");
|
||||
if (this.embedded) {
|
||||
return 'newest';
|
||||
}
|
||||
|
||||
let queryParam = this.$route.query['order'];
|
||||
let storedType = window.localStorage.getItem('photos_order');
|
||||
|
||||
if (queryParam) {
|
||||
window.localStorage.setItem("photos_order", queryParam);
|
||||
window.localStorage.setItem('photos_order', queryParam);
|
||||
return queryParam;
|
||||
} else if (storedType) {
|
||||
return storedType;
|
||||
}
|
||||
|
||||
return "newest";
|
||||
return 'newest';
|
||||
},
|
||||
openLocation(index) {
|
||||
if (!this.hasPlaces || !this.canSearchPlaces) {
|
||||
|
@ -292,9 +304,9 @@ export default {
|
|||
}
|
||||
|
||||
if (photo.CellID && photo.CellID !== "zz") {
|
||||
this.$router.push({name: "places_query", params: {q: photo.CellID}});
|
||||
this.$router.push({name: "places", query: {q: photo.CellID}});
|
||||
} else if (photo.Country && photo.Country !== "zz") {
|
||||
this.$router.push({name: "places_query", params: {q: "country:" + photo.Country}});
|
||||
this.$router.push({name: "places", query: {q: "country:" + photo.Country}});
|
||||
} else {
|
||||
this.$notify.warn("unknown location");
|
||||
}
|
||||
|
@ -384,7 +396,7 @@ export default {
|
|||
if (this.complete) {
|
||||
this.setOffset(response.offset);
|
||||
|
||||
if (this.results.length > 1) {
|
||||
if (!this.embedded && this.results.length > 1) {
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} pictures found"), {n: this.results.length}));
|
||||
}
|
||||
} else if (this.results.length >= Photo.limit()) {
|
||||
|
@ -547,9 +559,9 @@ export default {
|
|||
if (this.complete) {
|
||||
if (!this.results.length) {
|
||||
this.$notify.warn(this.$gettext("No pictures found"));
|
||||
} else if (this.results.length === 1) {
|
||||
} else if (!this.embedded && this.results.length === 1) {
|
||||
this.$notify.info(this.$gettext("One picture found"));
|
||||
} else {
|
||||
} else if (!this.embedded) {
|
||||
this.$notify.info(this.$gettextInterpolate(this.$gettext("%{n} pictures found"), {n: this.results.length}));
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -18,17 +18,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div id="map" ref="map" style="width: 100%; height: 100%;"></div>
|
||||
<div v-if="showCluster" class="map-control cluster-control">
|
||||
<v-card class="cluster-control-container">
|
||||
<p-page-photos
|
||||
ref="cluster"
|
||||
:static-filter="cluster"
|
||||
:on-close="closeCluster"
|
||||
:embedded="true"
|
||||
/>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
<v-dialog v-model="showClusterPictures" overflowed width="100%">
|
||||
<v-card min-height="80vh">
|
||||
<p-page-photos
|
||||
v-if="showClusterPictures"
|
||||
:static-filter="selectedClusterBounds"
|
||||
:on-close="unselectCluster"
|
||||
sticky-toolbar
|
||||
/>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
@ -75,57 +75,25 @@ export default {
|
|||
result: {},
|
||||
filter: {q: this.query(), s: this.scope()},
|
||||
lastFilter: {},
|
||||
cluster: {},
|
||||
showCluster: false,
|
||||
config: this.$config.values,
|
||||
settings: settings,
|
||||
animate: settings.animate,
|
||||
selectedClusterBounds: undefined,
|
||||
showClusterPictures: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
'$route'() {
|
||||
|
||||
const clusterWasOpenBeforeRouterChange = this.selectedClusterBounds !== undefined;
|
||||
const clusterIsOpenAfterRouteChange = this.getSelectedClusterFromUrl() !== undefined;
|
||||
const lastRouteChangeWasClusterOpenOrClose = clusterWasOpenBeforeRouterChange !== clusterIsOpenAfterRouteChange;
|
||||
|
||||
if (lastRouteChangeWasClusterOpenOrClose) {
|
||||
this.updateSelectedClusterFromUrl();
|
||||
|
||||
/**
|
||||
* dont touch any filters or searches if the only action taken was
|
||||
* opening or closing a cluster.
|
||||
* This currently assumes that when a cluster was opened or closed,
|
||||
* nothing else changed. I currently can't think of a scenario, where
|
||||
* a route-change is triggered by the user wanting to open/close a cluster
|
||||
* AND for example update the filter at the same time.
|
||||
*
|
||||
* Without this, opening or closing a cluster triggers a search, even
|
||||
* though no search parameter changed. Also without this, closing a
|
||||
* cluster resets the filter, because closing a cluster is done via
|
||||
* backwards navigation.
|
||||
* (closing is cluster is done via backwards navigation so that it can
|
||||
* be closed using the back-button. This is especially useful on android
|
||||
* smartphones)
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
this.filter.q = this.query();
|
||||
this.filter.s = this.scope();
|
||||
this.lastFilter = {};
|
||||
this.initialized = false;
|
||||
|
||||
this.search();
|
||||
},
|
||||
showClusterPictures: function (newValue, old) {
|
||||
if (!newValue) {
|
||||
this.unselectCluster();
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initMap().then(() => this.renderMap());
|
||||
this.updateSelectedClusterFromUrl();
|
||||
this.openClusterFromUrl();
|
||||
},
|
||||
methods: {
|
||||
initMap() {
|
||||
|
@ -341,61 +309,71 @@ export default {
|
|||
this.filter = filter;
|
||||
this.options = mapOptions;
|
||||
},
|
||||
getSelectedClusterFromUrl() {
|
||||
const clusterIsSelected = this.$route.query.selectedCluster !== undefined
|
||||
&& this.$route.query.selectedCluster !== '';
|
||||
if (!clusterIsSelected) {
|
||||
getClusterFromUrl() {
|
||||
const hasLatLng = this.$route.query.latlng !== undefined && this.$route.query.latlng !== '';
|
||||
|
||||
if (!hasLatLng) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [latmin, latmax, lngmin, lngmax] = this.$route.query.selectedCluster.split(',');
|
||||
return {latmin, latmax, lngmin, lngmax};
|
||||
return {
|
||||
q: this.filter.q,
|
||||
s: this.filter.s,
|
||||
latlng: this.$route.query.latlng,
|
||||
};
|
||||
},
|
||||
updateSelectedClusterFromUrl: function () {
|
||||
this.selectedClusterBounds = this.getSelectedClusterFromUrl();
|
||||
this.showClusterPictures = this.selectedClusterBounds !== undefined;
|
||||
openCluster: function (cluster) {
|
||||
this.cluster = cluster;
|
||||
this.showCluster = true;
|
||||
},
|
||||
selectClusterByCoords: function (latMin, latMax, lngMin, lngMax) {
|
||||
this.$router.push({
|
||||
query: {
|
||||
selectedCluster: [latMin, latMax, lngMin, lngMax].join(','),
|
||||
},
|
||||
params: this.filter,
|
||||
openClusterFromUrl: function () {
|
||||
const cluster = this.getClusterFromUrl();
|
||||
|
||||
if (!cluster) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openCluster(cluster);
|
||||
},
|
||||
selectClusterByCoords: function (latNorth, lngEast, latSouth, lngWest) {
|
||||
this.openCluster({
|
||||
q: this.filter.q,
|
||||
s: this.filter.s,
|
||||
latlng: [latNorth, lngEast, latSouth, lngWest].join(','),
|
||||
});
|
||||
},
|
||||
selectClusterById: function (clusterId) {
|
||||
if(this.showCluster) {
|
||||
this.showCluster = false;
|
||||
}
|
||||
|
||||
this.getClusterFeatures(clusterId, -1, (clusterFeatures) => {
|
||||
let latMin, latMax, lngMin, lngMax;
|
||||
let latNorth, lngEast, latSouth, lngWest;
|
||||
for (const feature of clusterFeatures) {
|
||||
const [lng, lat] = feature.geometry.coordinates;
|
||||
if (latMin === undefined || lat < latMin) {
|
||||
latMin = lat;
|
||||
if (latNorth === undefined || lat < latNorth) {
|
||||
latNorth = lat;
|
||||
}
|
||||
if (latMax === undefined || lat > latMax) {
|
||||
latMax = lat;
|
||||
if (lngEast === undefined || lng < lngEast) {
|
||||
lngEast = lng;
|
||||
}
|
||||
if (lngMin === undefined || lng < lngMin) {
|
||||
lngMin = lng;
|
||||
if (latSouth === undefined || lat > latSouth) {
|
||||
latSouth = lat;
|
||||
}
|
||||
if (lngMax === undefined || lng > lngMax) {
|
||||
lngMax = lng;
|
||||
if (lngWest === undefined || lng > lngWest) {
|
||||
lngWest = lng;
|
||||
}
|
||||
}
|
||||
|
||||
this.selectClusterByCoords(latMin, latMax, lngMin, lngMax);
|
||||
this.selectClusterByCoords(latNorth, lngEast, latSouth, lngWest);
|
||||
});
|
||||
},
|
||||
unselectCluster: function () {
|
||||
const aClusterIsSelected = this.getSelectedClusterFromUrl() !== undefined;
|
||||
if (aClusterIsSelected) {
|
||||
// it shouldn't matter wether a cluster was closed by pressing the back
|
||||
// button on a browser or the x-button on the dialog. We therefore make
|
||||
// both actions do the exact same thing: navigate backwards
|
||||
this.$router.go(-1);
|
||||
}
|
||||
closeCluster: function () {
|
||||
this.cluster = {};
|
||||
this.showCluster = false;
|
||||
},
|
||||
query: function () {
|
||||
return this.$route.params.q ? this.$route.params.q : '';
|
||||
return this.$route.query.q ? this.$route.query.q : '';
|
||||
},
|
||||
scope: function () {
|
||||
return this.$route.params.s ? this.$route.params.s : '';
|
||||
|
@ -437,11 +415,21 @@ export default {
|
|||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
this.search();
|
||||
|
||||
this.$router.push({
|
||||
query: {
|
||||
q: this.filter.q,
|
||||
},
|
||||
});
|
||||
},
|
||||
clearQuery() {
|
||||
this.filter.q = '';
|
||||
this.search();
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$router.push({
|
||||
query: {},
|
||||
});
|
||||
},
|
||||
updateQuery() {
|
||||
if (this.loading) {
|
||||
|
@ -450,9 +438,9 @@ export default {
|
|||
|
||||
if (this.query() !== this.filter.q) {
|
||||
if (this.filter.s) {
|
||||
this.$router.replace({name: "places_scope", params: {s: this.filter.s, q: this.filter.q}});
|
||||
this.$router.replace({name: "places_scope", params: {s: this.filter.s}, query: {q: this.filter.q}});
|
||||
} else if (this.filter.q) {
|
||||
this.$router.replace({name: "places_query", params: {q: this.filter.q}});
|
||||
this.$router.replace({name: "places", query: {q: this.filter.q}});
|
||||
} else {
|
||||
this.$router.replace({name: "places"});
|
||||
}
|
||||
|
@ -477,10 +465,12 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
// Don't query the same data more than once
|
||||
if (JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return;
|
||||
// Do not query the same data more than once unless search results need to be updated.
|
||||
if (this.initialized && JSON.stringify(this.lastFilter) === JSON.stringify(this.filter)) return;
|
||||
this.loading = true;
|
||||
|
||||
this.closeCluster();
|
||||
|
||||
Object.assign(this.lastFilter, this.filter);
|
||||
|
||||
this.updateQuery();
|
||||
|
@ -614,7 +604,10 @@ export default {
|
|||
// Is it a cluster?
|
||||
if (props.cluster) {
|
||||
// Update cluster marker.
|
||||
let id = -1*props.cluster_id;
|
||||
|
||||
// Attention: Do not confuse with photo feature IDs.
|
||||
// Clusters have their own ID number range!
|
||||
let id = -1 * props.cluster_id;
|
||||
|
||||
let marker = this.markers[id];
|
||||
|
||||
|
@ -636,7 +629,7 @@ export default {
|
|||
const previewImageCount = clusterFeatures.length >= 4 ? 4 : clusterFeatures.length > 1 ? 2 : 1;
|
||||
const images = Array(previewImageCount)
|
||||
.fill(null)
|
||||
.map((a,i) => {
|
||||
.map((a, i) => {
|
||||
const feature = clusterFeatures[Math.floor(clusterFeatures.length * i / previewImageCount)];
|
||||
const image = document.createElement('div');
|
||||
image.style.backgroundImage = `url(${this.$config.contentUri}/t/${feature.properties.Hash}/${token}/tile_${50})`;
|
||||
|
@ -724,6 +717,11 @@ export default {
|
|||
type: 'circle',
|
||||
source: 'photos',
|
||||
filter: ['has', 'point_count'],
|
||||
paint: {
|
||||
'circle-color': '#FFFFFF',
|
||||
'circle-opacity': 0,
|
||||
'circle-radius': 0,
|
||||
},
|
||||
});
|
||||
|
||||
// Example of dynamic map cluster rendering:
|
||||
|
|
|
@ -43,13 +43,12 @@ type SearchPhotos struct {
|
|||
Private bool `form:"private" notes:"Finds private pictures"`
|
||||
Favorite string `form:"favorite" example:"favorite:yes" notes:"Finds favorites only"`
|
||||
Unsorted bool `form:"unsorted" notes:"Finds pictures not in an album"`
|
||||
Lat float32 `form:"lat" notes:"Latitude (GPS Position)"`
|
||||
Lng float32 `form:"lng" notes:"Longitude (GPS Position)"`
|
||||
Latmin float32 `form:"latmin" notes:"Minimum latitude (GPS Position)"`
|
||||
Latmax float32 `form:"latmax" notes:"Maximum latitude (GPS Position)"`
|
||||
Lngmin float32 `form:"lngmin" notes:"Minimum longitude (GPS Position)"`
|
||||
Lngmax float32 `form:"lngmax" notes:"Maximum longitude (GPS Position)"`
|
||||
Dist uint `form:"dist" example:"dist:5" notes:"Distance in km in combination with lat/lng"`
|
||||
Lat float32 `form:"lat" notes:"GPS Position (Latitude)"`
|
||||
Lng float32 `form:"lng" notes:"GPS Position (Longitude)"`
|
||||
Dist uint `form:"dist" example:"dist:5" notes:"Distance to GPS Position (km)"`
|
||||
LatLng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"`
|
||||
S2 string `form:"s2" notes:"S2 Position (Cell ID)"`
|
||||
OLC string `form:"olc" notes:"Open Location Code (OLC)"`
|
||||
Fmin float32 `form:"fmin" notes:"F-number (min)"`
|
||||
Fmax float32 `form:"fmax" notes:"F-number (max)"`
|
||||
Chroma int16 `form:"chroma" example:"chroma:70" notes:"Chroma (0-100)"`
|
||||
|
|
|
@ -42,11 +42,12 @@ type SearchPhotosGeo struct {
|
|||
Face string `form:"face" notes:"Face ID, yes, no, new, or kind"`
|
||||
Faces string `form:"faces"` // Find or exclude faces if detected.
|
||||
Subject string `form:"subject"`
|
||||
Lat float32 `form:"lat"`
|
||||
Lng float32 `form:"lng"`
|
||||
S2 string `form:"s2"`
|
||||
Olc string `form:"olc"`
|
||||
Dist uint `form:"dist"`
|
||||
Lat float32 `form:"lat" notes:"GPS Position (Latitude)"`
|
||||
Lng float32 `form:"lng" notes:"GPS Position (Longitude)"`
|
||||
Dist uint `form:"dist" example:"dist:5" notes:"Distance to GPS Position (km)"`
|
||||
LatLng string `form:"latlng" notes:"GPS Bounding Box (Lat N, Lng E, Lat S, Lng W)"`
|
||||
S2 string `form:"s2" notes:"S2 Position (Cell ID)"`
|
||||
OLC string `form:"olc" notes:"Open Location Code (OLC)"`
|
||||
Person string `form:"person"` // Alias for Subject
|
||||
Subjects string `form:"subjects"` // Text
|
||||
People string `form:"people"` // Alias for Subjects
|
||||
|
|
|
@ -13,8 +13,11 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
"github.com/photoprism/photoprism/pkg/sortby"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
@ -55,6 +58,9 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
|||
return PhotoResults{}, 0, ErrBadRequest
|
||||
}
|
||||
|
||||
// Size of S2 Cells.
|
||||
S2Levels := 7
|
||||
|
||||
// Specify table names and joins.
|
||||
s := UnscopedDb().Table(entity.File{}.TableName()).Select(resultCols).
|
||||
Joins("JOIN photos ON files.photo_id = photos.id AND files.media_id IS NOT NULL").
|
||||
|
@ -622,35 +628,46 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
|||
s = s.Where("photos.photo_f_number <= ?", f.Fmax)
|
||||
}
|
||||
|
||||
// Filter by location.
|
||||
if f.S2 != "" {
|
||||
// S2 Cell ID.
|
||||
s2Min, s2Max := s2.PrefixedRange(f.S2, S2Levels)
|
||||
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)
|
||||
} else if f.OLC != "" {
|
||||
// Open Location Code (OLC).
|
||||
s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.OLC), S2Levels)
|
||||
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)
|
||||
} else if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.LatLng); parseErr == nil {
|
||||
// GPS Bounds (Lat N, Lng E, Lat S, Lng W).
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest)
|
||||
}
|
||||
|
||||
// Filter by approx distance to coordinates.
|
||||
if f.Dist == 0 {
|
||||
f.Dist = 20
|
||||
} else if f.Dist > 5000 {
|
||||
f.Dist = 5000
|
||||
}
|
||||
|
||||
// Filter by approx distance to co-ordinates:
|
||||
if f.Lat != 0 {
|
||||
latMin := f.Lat - Radius*float32(f.Dist)
|
||||
latMax := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
latNorth := f.Lat - Radius*float32(f.Dist)
|
||||
latSouth := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth)
|
||||
}
|
||||
|
||||
if f.Lng != 0 {
|
||||
lngMin := f.Lng - Radius*float32(f.Dist)
|
||||
lngMax := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
|
||||
if f.Latmin != 0 && f.Latmax != 0 {
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", f.Latmin, f.Latmax)
|
||||
}
|
||||
if f.Lngmin != 0 && f.Lngmax != 0 {
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", f.Lngmin, f.Lngmax)
|
||||
lngEast := f.Lng - Radius*float32(f.Dist)
|
||||
lngWest := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest)
|
||||
}
|
||||
|
||||
// Find photos taken before date.
|
||||
if !f.Before.IsZero() {
|
||||
s = s.Where("photos.taken_at <= ?", f.Before.Format("2006-01-02"))
|
||||
}
|
||||
|
||||
// Find photos taken after date.
|
||||
if !f.After.IsZero() {
|
||||
s = s.Where("photos.taken_at >= ?", f.After.Format("2006-01-02"))
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
|
@ -38,6 +39,7 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
return GeoResults{}, ErrBadRequest
|
||||
}
|
||||
|
||||
// Size of S2 Cells.
|
||||
S2Levels := 7
|
||||
|
||||
// Search for nearby photos.
|
||||
|
@ -50,8 +52,6 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
}
|
||||
|
||||
f.S2 = photo.CellID
|
||||
f.Lat = photo.PhotoLat
|
||||
f.Lng = photo.PhotoLng
|
||||
|
||||
S2Levels = 12
|
||||
}
|
||||
|
@ -509,24 +509,38 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
|||
s = s.Where("files.file_chroma > 0 AND files.file_chroma <= ?", f.Chroma)
|
||||
}
|
||||
|
||||
// Filter by location.
|
||||
if f.S2 != "" {
|
||||
// S2 Cell ID.
|
||||
s2Min, s2Max := s2.PrefixedRange(f.S2, S2Levels)
|
||||
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)
|
||||
} else if f.Olc != "" {
|
||||
s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.Olc), S2Levels)
|
||||
} else if f.OLC != "" {
|
||||
// Open Location Code (OLC).
|
||||
s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.OLC), S2Levels)
|
||||
s = s.Where("photos.cell_id BETWEEN ? AND ?", s2Min, s2Max)
|
||||
} else {
|
||||
// Filter by approx distance to coordinate:
|
||||
if f.Lat != 0 {
|
||||
latMin := f.Lat - Radius*float32(f.Dist)
|
||||
latMax := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latMin, latMax)
|
||||
}
|
||||
if f.Lng != 0 {
|
||||
lngMin := f.Lng - Radius*float32(f.Dist)
|
||||
lngMax := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngMin, lngMax)
|
||||
}
|
||||
} else if latNorth, lngEast, latSouth, lngWest, parseErr := clean.GPSBounds(f.LatLng); parseErr == nil {
|
||||
// GPS Bounds (Lat N, Lng E, Lat S, Lng W).
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest)
|
||||
}
|
||||
|
||||
// Filter by approx distance to coordinates.
|
||||
if f.Dist == 0 {
|
||||
f.Dist = 20
|
||||
} else if f.Dist > 5000 {
|
||||
f.Dist = 5000
|
||||
}
|
||||
|
||||
if f.Lat != 0 {
|
||||
latNorth := f.Lat - Radius*float32(f.Dist)
|
||||
latSouth := f.Lat + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lat BETWEEN ? AND ?", latNorth, latSouth)
|
||||
}
|
||||
|
||||
if f.Lng != 0 {
|
||||
lngEast := f.Lng - Radius*float32(f.Dist)
|
||||
lngWest := f.Lng + Radius*float32(f.Dist)
|
||||
s = s.Where("photos.photo_lng BETWEEN ? AND ?", lngEast, lngWest)
|
||||
}
|
||||
|
||||
// Find photos taken before date.
|
||||
|
|
|
@ -258,6 +258,7 @@ func TestPhotosGeoQueryNear(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, len(photos), 8)
|
||||
})
|
||||
t.Run("pr2xu7myk7wrbk30", func(t *testing.T) {
|
||||
|
|
|
@ -130,7 +130,7 @@ func TestGeo(t *testing.T) {
|
|||
Lat: 1.234,
|
||||
Lng: 4.321,
|
||||
S2: "",
|
||||
Olc: "",
|
||||
OLC: "",
|
||||
Dist: 0,
|
||||
Quality: 0,
|
||||
Review: true,
|
||||
|
@ -158,7 +158,7 @@ func TestGeo(t *testing.T) {
|
|||
Lat: 0,
|
||||
Lng: 0,
|
||||
S2: "",
|
||||
Olc: "",
|
||||
OLC: "",
|
||||
Dist: 0,
|
||||
Quality: 3,
|
||||
Review: false,
|
||||
|
@ -181,7 +181,7 @@ func TestGeo(t *testing.T) {
|
|||
Lat: 0,
|
||||
Lng: 0,
|
||||
S2: "85",
|
||||
Olc: "",
|
||||
OLC: "",
|
||||
Dist: 0,
|
||||
Quality: 0,
|
||||
Review: false,
|
||||
|
@ -195,7 +195,7 @@ func TestGeo(t *testing.T) {
|
|||
assert.Empty(t, result)
|
||||
assert.IsType(t, GeoResults{}, result)
|
||||
})
|
||||
t.Run("search for Olc", func(t *testing.T) {
|
||||
t.Run("search for OLC", func(t *testing.T) {
|
||||
f := form.SearchPhotosGeo{
|
||||
Query: "",
|
||||
Before: time.Time{},
|
||||
|
@ -204,7 +204,7 @@ func TestGeo(t *testing.T) {
|
|||
Lat: 0,
|
||||
Lng: 0,
|
||||
S2: "",
|
||||
Olc: "9",
|
||||
OLC: "9",
|
||||
Dist: 0,
|
||||
Quality: 0,
|
||||
Review: false,
|
||||
|
|
|
@ -667,9 +667,9 @@ func TestPhotos(t *testing.T) {
|
|||
assert.LessOrEqual(t, 2, len(photos))
|
||||
|
||||
})
|
||||
t.Run("Latmin:33.45343166666667 Latmax:49.519234", func(t *testing.T) {
|
||||
t.Run("LatLng:33.453431,-180.0,49.519234,180.0", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = "Latmin:33.45343166666667 Latmax:49.519234"
|
||||
f.Query = "LatLng:33.453431,-180.0,49.519234,180.0"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
f.Order = "imported"
|
||||
|
@ -688,9 +688,9 @@ func TestPhotos(t *testing.T) {
|
|||
assert.LessOrEqual(t, 2, len(photos))
|
||||
|
||||
})
|
||||
t.Run("Latmin:0.00 Latmax:49.519234 Lngmin:-30.123 Lngmax:9.1001234", func(t *testing.T) {
|
||||
t.Run("LatLng:0.00,-30.123.0,49.519234,9.1001234", func(t *testing.T) {
|
||||
var f form.SearchPhotos
|
||||
f.Query = "Latmin:0.00 Latmax:49.519234 Lngmin:-30.123 Lngmax:9.1001234"
|
||||
f.Query = "LatLng:0.00,-30.123.0,49.519234,9.1001234"
|
||||
f.Count = 10
|
||||
f.Offset = 0
|
||||
f.Order = "imported"
|
||||
|
|
62
pkg/clean/gps.go
Normal file
62
pkg/clean/gps.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GPSBounds parses the GPS bounds (Lat N, Lng E, Lat S, Lng W) and returns the coordinates if any.
|
||||
func GPSBounds(bounds string) (latNorth, lngEast, latSouth, lngWest float32, err error) {
|
||||
if len(bounds) < 7 {
|
||||
return 0, 0, 0, 0, fmt.Errorf("no coordinates found")
|
||||
}
|
||||
|
||||
values := strings.SplitN(bounds, ",", 5)
|
||||
found := len(values)
|
||||
|
||||
// Invalid number of values?
|
||||
if found != 4 {
|
||||
return 0, 0, 0, 0, fmt.Errorf("invalid number of coordinates")
|
||||
}
|
||||
|
||||
// Parse floating point coordinates.
|
||||
latNorth, lngEast, latSouth, lngWest = txt.Float32(values[0]), txt.Float32(values[1]), txt.Float32(values[2]), txt.Float32(values[3])
|
||||
|
||||
// Latitudes (from +90 to -90 degrees).
|
||||
if latNorth > 90 {
|
||||
latNorth = 90
|
||||
} else if latNorth < -90 {
|
||||
latNorth = -90
|
||||
}
|
||||
|
||||
if latSouth > 90 {
|
||||
latSouth = 90
|
||||
} else if latSouth < -90 {
|
||||
latSouth = -90
|
||||
}
|
||||
|
||||
if latNorth > latSouth {
|
||||
latNorth, latSouth = latSouth, latNorth
|
||||
}
|
||||
|
||||
// Longitudes (from -180 to 180 degrees).
|
||||
if lngEast > 180 {
|
||||
lngEast = 180
|
||||
} else if lngEast < -180 {
|
||||
lngEast = -180
|
||||
}
|
||||
|
||||
if lngWest > 180 {
|
||||
lngWest = 180
|
||||
} else if lngWest < -180 {
|
||||
lngWest = -180
|
||||
}
|
||||
|
||||
if lngEast > lngWest {
|
||||
lngEast, lngWest = lngWest, lngEast
|
||||
}
|
||||
|
||||
return latNorth, lngEast, latSouth, lngWest, nil
|
||||
}
|
66
pkg/clean/gps_test.go
Normal file
66
pkg/clean/gps_test.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
package clean
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGPSBounds(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.62521362304688,41.89404296875,-87.6215591430664")
|
||||
assert.Equal(t, float32(41.87760543823242), latNorth)
|
||||
assert.Equal(t, float32(-87.62521362304688), lngEast)
|
||||
assert.Equal(t, float32(41.89404296875), latSouth)
|
||||
assert.Equal(t, float32(-87.6215591430664), lngWest)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("FlippedLat", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.89404296875,-87.62521362304688,41.87760543823242,-87.6215591430664")
|
||||
assert.Equal(t, float32(41.87760543823242), latNorth)
|
||||
assert.Equal(t, float32(-87.62521362304688), lngEast)
|
||||
assert.Equal(t, float32(41.89404296875), latSouth)
|
||||
assert.Equal(t, float32(-87.6215591430664), lngWest)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("FlippedLng", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.6215591430664,41.89404296875,-87.62521362304688")
|
||||
assert.Equal(t, float32(41.87760543823242), latNorth)
|
||||
assert.Equal(t, float32(-87.62521362304688), lngEast)
|
||||
assert.Equal(t, float32(41.89404296875), latSouth)
|
||||
assert.Equal(t, float32(-87.6215591430664), lngWest)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("")
|
||||
assert.Equal(t, float32(0), latNorth)
|
||||
assert.Equal(t, float32(0), lngEast)
|
||||
assert.Equal(t, float32(0), latSouth)
|
||||
assert.Equal(t, float32(0), lngWest)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("One", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242")
|
||||
assert.Equal(t, float32(0), latNorth)
|
||||
assert.Equal(t, float32(0), lngEast)
|
||||
assert.Equal(t, float32(0), latSouth)
|
||||
assert.Equal(t, float32(0), lngWest)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Three", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.62521362304688,41.89404296875")
|
||||
assert.Equal(t, float32(0), latNorth)
|
||||
assert.Equal(t, float32(0), lngEast)
|
||||
assert.Equal(t, float32(0), latSouth)
|
||||
assert.Equal(t, float32(0), lngWest)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
t.Run("Five", func(t *testing.T) {
|
||||
latNorth, lngEast, latSouth, lngWest, err := GPSBounds("41.87760543823242,-87.62521362304688,41.89404296875,-87.6215591430664,41.89404296875")
|
||||
assert.Equal(t, float32(0), latNorth)
|
||||
assert.Equal(t, float32(0), lngEast)
|
||||
assert.Equal(t, float32(0), latSouth)
|
||||
assert.Equal(t, float32(0), lngWest)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
|
@ -65,6 +65,11 @@ func Float(s string) float64 {
|
|||
return f
|
||||
}
|
||||
|
||||
// Float32 converts a string to a 32-bit floating point number or 0 if invalid.
|
||||
func Float32(s string) float32 {
|
||||
return float32(Float(s))
|
||||
}
|
||||
|
||||
// Int64 converts a string to a signed 64-bit integer or 0 if invalid.
|
||||
func Int64(s string) int64 {
|
||||
if s == "" {
|
||||
|
|
Loading…
Reference in a new issue