@panoramax/web-viewer
Version:
Panoramax web viewer for geolocated pictures
430 lines (396 loc) • 12.6 kB
JavaScript
import { LitElement, html, css, nothing } from "lit";
import { fa } from "../../utils/widgets";
import { getUserAccount } from "../../utils/utils";
import { faSvg, titles } from "../styles";
import { faImage } from "@fortawesome/free-solid-svg-icons/faImage";
import { faCalendar } from "@fortawesome/free-solid-svg-icons/faCalendar";
import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight";
import { faMedal } from "@fortawesome/free-solid-svg-icons/faMedal";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle";
import { faUser } from "@fortawesome/free-solid-svg-icons/faUser";
import { onceParentAvailable } from "../../utils/widgets";
/**
* Map Filters menu allows user to select map data they want displayed.
* @class Panoramax.components.menus.MapFilters
* @element pnx-map-filters-menu
* @extends [lit.LitElement](https://lit.dev/docs/api/LitElement/)
* @example
* ```html
* <pnx-map-filters-menu user-search="" _parent=${viewer} />
* ```
*/
export default class MapFilters extends LitElement {
/** @private */
static styles = [ faSvg, titles, css`
.pnx-input-group {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 5px;
}
/* Filter block */
.pnx-filter-block {
position: relative;
padding: 10px 15px;
border-bottom: 2px solid var(--widget-border-div);
}
.pnx-filter-block:first-child {
padding-top: 15px;
}
.pnx-filter-block:last-child {
border-bottom: none;
padding-bottom: 15px;
}
.pnx-filter-zoomin {
z-index: 131;
background-color: rgba(255,255,255,0.8);
text-align: center;
font-weight: 800;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
border-radius: 25px;
}
.pnx-filter-zoomin.hidden {
display: none;
}
/* Input styles */
.pnx-filter-active,
pnx-search-bar.pnx-filter-active::part(container),
pnx-search-bar.pnx-filter-active::part(input) {
background-color: var(--widget-bg-active) ;
border-color: var(--widget-bg-active) ;
color: var(--widget-font-active) ;
}
input[type=date] {
min-width: 0;
flex-grow: 2;
padding: 2px 0;
text-align: center;
background-color: var(--widget-bg);
color: var(--widget-font);
border: 1px solid var(--widget-border-div);
border-radius: 20px;
font-family: var(--font-family);
}
/* Input shortcuts */
.pnx-input-shortcuts {
margin-top: -10px;
margin-bottom: 5px;
}
.pnx-input-shortcuts button {
border: none;
font-size: 0.75em;
padding: 2px 6px;
vertical-align: middle;
background-color: var(--grey-pale);
color: var(--black);
border-radius: 10px;
cursor: pointer;
font-family: var(--font-family);
}
.pnx-input-shortcuts button:hover {
background-color: #d9dcd9;
}
/* Checkbox looking like buttons */
.pnx-input-group.pnx-checkbox-btns {
gap: 0;
align-items: stretch;
}
.pnx-checkbox-btns label {
display: inline-block;
padding: 2px 7px;
background: none;
border: 1px solid var(--widget-border-btn);
color: var(--widget-font-direct);
cursor: pointer;
font-size: 16px;
text-decoration: none;
border-left-width: 0px;
}
.pnx-checkbox-btns label:hover {
background-color: var(--widget-bg-hover);
}
.pnx-checkbox-btns label:first-of-type {
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
border-left-width: 1px;
}
.pnx-checkbox-btns label:last-of-type {
border-top-right-radius: 8px;
border-bottom-right-radius: 8px;
}
.pnx-checkbox-btns input[type="radio"] { display: none; }
.pnx-checkbox-btns input[type="radio"]:checked + label {
background-color: var(--widget-bg-active);
color: var(--widget-font-active);
}
.pnx-checkbox-btns input[type="radio"]:checked + label:first-of-type {
border-right-color: white;
}
/* Force user search width */
#pnx-filter-search-user::part(container) { width: 100%; }
` ];
/**
* Component properties.
* @memberof Panoramax.components.menus.MapFilters#
* @type {Object}
* @property {boolean} [user-search=false] Should user search filter show up ?
* @property {boolean} [quality-score=false] Should quality score filter show up ?
* @property {boolean} [no-date=false] Should date filters be hidden ?
* @property {boolean} [no-picture-type=false] Should picture type filter be hidden ?
*/
static properties = {
"quality-score": {type: Boolean},
"user-search": {type: Boolean},
"no-date": {type: Boolean},
"no-picture-type": {type: Boolean},
showZoomIn: {state: true},
minDate: {state: true},
maxDate: {state: true},
type: {state: true},
score: {state: true},
user: {state: true},
};
constructor() {
super();
this._formDelay = null;
this.showZoomIn = true;
}
/** @private */
connectedCallback() {
super.connectedCallback();
// Input changes
for(let i of this.shadowRoot.querySelectorAll("input")) {
i.addEventListener("change", this._onFormChange.bind(this));
i.addEventListener("keypress", this._onFormChange.bind(this));
i.addEventListener("paste", this._onFormChange.bind(this));
i.addEventListener("input", this._onFormChange.bind(this));
}
// Map zoom
onceParentAvailable(this).then(() => this._parent.onceMapReady?.().then(async () => {
this._parent.map.on("zoomend", this._onMapZoom.bind(this));
this._parent.map.on("filters-changed", this._onParentFilterChange.bind(this));
this._onMapZoom();
this._onParentFilterChange(this._parent.map._mapFilters);
// Load default users filter
const vu = this._parent.map.getVisibleUsers();
if(vu?.length > 0 && vu[0] != "geovisio") {
this.user = vu[0];
const username = await this._parent.api.getUserName(vu[0]);
if(username) { this.user = username; }
}
}));
}
/**
* Map zoom event handler: show/hide "zoom in" labels
* @private
*/
_onMapZoom() {
this.showZoomIn = this._parent.map.getZoom() < 7;
}
/**
* Filter changes on parent: update input fields
* @private
*/
_onParentFilterChange(e) {
this.minDate = e?.minDate || null;
this.maxDate = e?.maxDate || null;
// Sanity check for date
if(this.minDate && this.maxDate && this.minDate > this.maxDate) {
const prevMin = this.minDate;
this.minDate = this.maxDate;
this.maxDate = prevMin;
this._onFormChange();
}
this.score = e?.qualityscore?.length < 5 ? e.qualityscore.join(",") : "";
this.type = "";
if(e?.pic_type && e.pic_type != "") {
this.type = e.pic_type == "flat" ? "flat" : "equirectangular";
}
}
/** @private */
_onSubmit(e) {
e.preventDefault();
this._onFormChange();
return false;
}
/** @private */
_onFormChange() {
if(this._formDelay) { clearTimeout(this._formDelay); }
this._formDelay = setTimeout(() => this._parent?._onMapFiltersChange(), 250);
}
/** @private */
_userSearch(value) {
return this?._parent.api.searchUsers(value)
.then(data => ((data || [])
.map(f => ({
title: f.label,
data: f
}))
));
}
/** @private */
_onUserSearchResult(e) {
if(e.detail) { e.target.classList.add("pnx-filter-active"); }
else { e.target.classList.remove("pnx-filter-active"); }
return this._parent?.map?.setVisibleUsers(e.detail?.data ? [e.detail.data.id] : ["geovisio"]);
}
/** @private */
_onReset() {
this.shadowRoot.querySelector("#pnx-filter-qualityscore")?.setAttribute("grade", "");
this.shadowRoot.querySelector("#pnx-filter-search-user")?.reset();
this.minDate = null;
this.maxDate = null;
this.type = "";
this.score = null;
this.user = null;
this._onFormChange();
}
/** @private */
_onDateShortcut(date) {
const dateFromField = this.shadowRoot.getElementById("pnx-filter-date-from");
const dateToField = this.shadowRoot.getElementById("pnx-filter-date-end");
if(dateFromField) {
if(dateFromField.value !== date) { dateFromField.value = date; }
else { dateFromField.value = ""; }
}
if(dateToField) { dateToField.value = ""; }
}
/** @private */
_onMeUserSearch() {
const userAccount = getUserAccount();
if(!userAccount) { return; }
const userField = this.shadowRoot.getElementById("pnx-filter-search-user");
if(!this._parent?.map?.getVisibleUsers().includes(userAccount.id)) {
userField?._onResultClick({title: userAccount.name, data: {id: userAccount.id }});
}
else {
userField?._onResultClick();
}
}
/** @private */
render() {
const userAccount = getUserAccount();
return html`<form
@reset=${this._onReset}
@change=${this._onFormChange}
@submit=${this._onSubmit}
>
${this["no-date"] ? "" : html`<div class="pnx-filter-block">
<div class="pnx-filter-zoomin ${this.showZoomIn ? "" : "hidden"}">${this._parent?._t.pnx.filter_zoom_in}</div>
<h4>${fa(faCalendar)} ${this._parent?._t.pnx.filter_date}</h4>
<div class="pnx-input-shortcuts">
<button
@click=${() => this._onDateShortcut(new Date(new Date().setMonth(new Date().getMonth() - 1)).toISOString().split("T")[0])}
>${this._parent?._t.pnx.filter_date_1month}</button>
<button
@click=${() => this._onDateShortcut(new Date(new Date().setMonth(new Date().getMonth() - 6)).toISOString().split("T")[0])}
>${this._parent?._t.pnx.filter_date_6months}</button>
<button
@click=${() => this._onDateShortcut(new Date(new Date().setFullYear(new Date().getFullYear() - 1)).toISOString().split("T")[0])}
>${this._parent?._t.pnx.filter_date_1year}</button>
</div>
<div class="pnx-input-group">
<input
type="date"
id="pnx-filter-date-from"
.value=${this.minDate}
class=${this.minDate && this.minDate != "" ? "pnx-filter-active" : ""}
/>
${fa(faArrowRight)}
<input
type="date"
id="pnx-filter-date-end"
.value=${this.maxDate}
class=${this.maxDate && this.maxDate != "" ? "pnx-filter-active" : ""}
/>
</div>
</div>`}
${this["no-picture-type"] ? "" : html`<div class="pnx-filter-block">
<h4>${fa(faImage)} ${this._parent?._t.pnx.filter_picture}</h4>
<div class="pnx-input-group pnx-checkbox-btns" style="justify-content: center;">
<input
type="radio"
id="pnx-filter-type-all"
name="pnx-filter-type"
value=""
.checked=${!this.type || this.type === ""}
/>
<label for="pnx-filter-type-all">${this._parent?._t.pnx.picture_all}</label>
<input
type="radio"
id="pnx-filter-type-flat"
name="pnx-filter-type"
value="flat"
.checked=${this.type === "flat"}
/>
<label for="pnx-filter-type-flat">${this._parent?._t.pnx.picture_flat}</label>
<input
type="radio"
id="pnx-filter-type-360"
name="pnx-filter-type"
value="equirectangular"
.checked=${this.type === "equirectangular"}
/>
<label for="pnx-filter-type-360">${this._parent?._t.pnx.picture_360}</label>
</div>
</div>`}
${this["quality-score"] ? html`
<div class="pnx-filter-block">
<div class="pnx-filter-zoomin ${this.showZoomIn ? "" : "hidden"}">${this._parent?._t.pnx.filter_zoom_in}</div>
<h4 style="margin-bottom: 3px">
${fa(faMedal)} ${this._parent?._t.pnx.filter_qualityscore}
<pnx-button
title="${this._parent?._t.pnx.metadata_quality_help}"
kind="superinline"
@click=${() => this._parent?._showQualityScoreDoc()}
>
${fa(faInfoCircle)}
</pnx-button>
</h4>
<div class="pnx-input-group">
<pnx-quality-score
id="pnx-filter-qualityscore"
_t=${this._parent?._t}
input="pnx-filter-qualityscore"
grade=${this.score}
@change=${this._onFormChange}
>
</pnx-quality-score>
</div>
</div>
` : nothing}
${this["user-search"] ? html`
<div class="pnx-filter-block">
<h4>${fa(faUser)} ${this._parent?._t.pnx.filter_user}</h4>
${userAccount ? html`
<div class="pnx-input-shortcuts">
<button @click=${this._onMeUserSearch}>
${this._parent?._t.pnx.filter_user_mypics}
</button>
</div>
` : nothing}
<pnx-search-bar
id="pnx-filter-search-user"
placeholder=${this._parent?._t.pnx.search_user}
class=${this.user ? "pnx-filter-active" : ""}
value=${this.user}
@result=${this._onUserSearchResult}
.searcher=${this._userSearch.bind(this)}
._parent=${this._parent}
no-menu-closure
>
</pnx-search-bar>
</div>
` : nothing}
</form>`;
}
}
customElements.define("pnx-map-filters-menu", MapFilters);