mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-03-30 08:00:41 +02:00
Allow Profile Imports to App, Map Sharing, iframe improvements (#102)
* update identiyf page * support non-ALPRs on Idenfity page * show pending devices, even w/o tags * update lprBaseTags * detect iframe instead of passing query string * implement share dialog * clean up identify * finishing touches for identify page
This commit is contained in:
@@ -8,7 +8,7 @@ import { useDiscordIntercept } from '@/composables/useDiscordIntercept';
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const isDark = computed(() => theme.name.value === 'dark');
|
||||
const isFullscreen = computed(() => router.currentRoute.value?.query.fullscreen === 'true');
|
||||
const isInIframe = computed(() => window.self !== window.top);
|
||||
const { showDialog, discordUrl, interceptDiscordLinks } = useDiscordIntercept();
|
||||
|
||||
function toggleTheme() {
|
||||
@@ -70,7 +70,7 @@ watch(() => theme.global.name.value, (newTheme) => {
|
||||
|
||||
<template>
|
||||
<v-app>
|
||||
<template v-if="!isFullscreen">
|
||||
<template v-if="!isInIframe">
|
||||
<v-app-bar
|
||||
flat
|
||||
prominent
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
:root {
|
||||
--df-background-color: white;
|
||||
--df-blue: rgb(18, 151, 195);
|
||||
--df-blue-dark: #0081ac;
|
||||
|
||||
/* Modern Typography Scale */
|
||||
--font-size-base: 1rem;
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
<template>
|
||||
<div style="position: relative">
|
||||
<v-btn v-if="showCopyButton" color="white" @click="copyToClipboard" icon variant="plain" flat class="copy-button">
|
||||
<v-btn v-if="props.showCopyButton" color="white" @click="copyToClipboard" icon variant="plain" flat class="copy-button">
|
||||
<v-icon class="copy-icon-with-shadow">mdi-content-copy</v-icon>
|
||||
</v-btn>
|
||||
<code ref="codeContent">
|
||||
<slot></slot>
|
||||
<code ref="codeContent" :class="{ 'code-darker': isDark }">
|
||||
<template v-if="osmTags">
|
||||
<template v-for="(value, key) in osmTags" :key="key">
|
||||
<span v-if="value !== ''">
|
||||
{{ key }}=<span :class="{ highlight: highlightValuesForKeys.includes(key)}">{{ value }}</span><br>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
<slot v-else></slot>
|
||||
</code>
|
||||
<v-snackbar color="#0081ac" v-model="snackbarOpen" :timeout="3000">
|
||||
Copied to clipboard!
|
||||
<v-snackbar color="var(--df-blue-dark)" v-model="snackbarOpen" :timeout="3000">
|
||||
<span class="text-white">Copied to clipboard!</span>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
color="white"
|
||||
variant="text"
|
||||
@click="snackbarOpen = false"
|
||||
>
|
||||
@@ -21,15 +29,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useTheme } from 'vuetify';
|
||||
|
||||
defineProps({
|
||||
showCopyButton: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
const theme = useTheme();
|
||||
const isDark = computed(() => theme.name.value === 'dark');
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
showCopyButton?: boolean;
|
||||
osmTags?: Record<string, string>;
|
||||
highlightValuesForKeys?: string[];
|
||||
}>(), {
|
||||
showCopyButton: true,
|
||||
highlightValuesForKeys: () => []
|
||||
});
|
||||
|
||||
const highlightValuesForKeys = computed(() => props.highlightValuesForKeys ?? []);
|
||||
|
||||
const codeContent = ref<HTMLElement | null>(null);
|
||||
const snackbarOpen = ref(false);
|
||||
|
||||
@@ -51,15 +67,15 @@ code {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: nowrap;
|
||||
.code-darker {
|
||||
background-color: rgb(22,22,22);
|
||||
}
|
||||
|
||||
.copy-icon-with-shadow {
|
||||
filter: drop-shadow(0px 0px 2px rgba(0, 0, 0, 1));
|
||||
/* Adjust shadow values as needed: horizontal-offset vertical-offset blur-radius color */
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
@@ -68,4 +84,11 @@ code {
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: #0081ac;
|
||||
padding: 0.15rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="map" :style="{
|
||||
height: isFullScreen ? '100dvh' : 'calc(100dvh - 64px)',
|
||||
marginTop: isFullScreen ? '0' : '64px',
|
||||
height: isIframe ? '100dvh' : 'calc(100dvh - 64px)',
|
||||
marginTop: isIframe ? '0' : '64px',
|
||||
}">
|
||||
<div class="topleft">
|
||||
<slot name="topleft"></slot>
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<div class="topright">
|
||||
<!-- Controls -->
|
||||
<div v-if="!isFullScreen" class="d-flex flex-column ga-2">
|
||||
<div v-if="!isIframe" class="d-flex flex-column ga-2">
|
||||
<!-- Clustering Toggle Switch -->
|
||||
<v-card variant="elevated">
|
||||
<v-card-text class="py-0">
|
||||
@@ -51,10 +51,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isIframe" class="bottomleft">
|
||||
<img src="/deflock-logo-grey.svg" alt="Deflock Logo" style="height: 24px; opacity: 0.75;" />
|
||||
</div>
|
||||
|
||||
<div class="bottomright">
|
||||
<v-btn icon to="/report" style="color: unset">
|
||||
<v-icon size="large">mdi-map-marker-plus</v-icon>
|
||||
</v-btn>
|
||||
<slot name="bottomright"></slot>
|
||||
</div>
|
||||
|
||||
@@ -89,7 +90,6 @@ import L, { type LatLngTuple, type FeatureGroup, type MarkerClusterGroup, type M
|
||||
import type { ALPR } from '@/types';
|
||||
import DFMapPopup from './DFMapPopup.vue';
|
||||
import { createVuetify } from 'vuetify'
|
||||
import { useRoute } from 'vue-router';
|
||||
import { computed } from 'vue';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'leaflet.markercluster';
|
||||
@@ -103,8 +103,7 @@ const CLUSTER_DISABLE_ZOOM = 16; // Clustering disabled at zoom 16 and above
|
||||
// Internal State Management
|
||||
const markerMap = new Map<string, Marker | CircleMarker>();
|
||||
const isInternalUpdate = ref(false);
|
||||
const route = useRoute();
|
||||
const isFullScreen = computed(() => route.query.fullscreen === 'true');
|
||||
const isIframe = computed(() => window.self !== window.top);
|
||||
|
||||
// Clustering Control
|
||||
const clusteringEnabled = ref(true);
|
||||
@@ -584,6 +583,13 @@ function registerMapEvents() {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.bottomleft {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
left: 4px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.bottomright {
|
||||
position: absolute;
|
||||
bottom: 25px;
|
||||
|
||||
@@ -32,16 +32,10 @@
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<h3 class="text-center serif">Tags to Copy</h3>
|
||||
<DFCode>
|
||||
<span v-for="(value, key) in lprBaseTags" :key="key">{{ key }}={{ value }}<br></span>
|
||||
<span v-for="(value, key) in selectedLprVendor?.osmTags" :key="key">{{ key }}=<span :class="highlightClass(selectedLprVendor)">{{ value }}</span><br></span>
|
||||
</DFCode>
|
||||
<DFCode :osm-tags="mergedTags" :highlight-values-for-keys="vendorTagKeys" />
|
||||
|
||||
<h5 class="text-center mt-4 serif">and if operator is known</h5>
|
||||
<DFCode :show-copy-button="false">
|
||||
operator=<span class="placeholder">[Enter operator name]</span><br>
|
||||
operator:wikidata=<span class="placeholder">[Enter WikiData ID]</span>
|
||||
</DFCode>
|
||||
<DFCode :osm-tags="operatorTags" :highlight-values-for-keys="['operator', 'operator:wikidata']" :show-copy-button="false" />
|
||||
<div class="text-caption text-center mt-1">
|
||||
<a href="https://www.wikidata.org/wiki/Wikidata:Main_Page" target="_blank" rel="noopener" class="text-decoration-none text-grey-darken-1">
|
||||
What is WikiData? <v-icon size="x-small">mdi-open-in-new</v-icon>
|
||||
@@ -53,7 +47,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DFCode from '@/components/DFCode.vue';
|
||||
import { ref, type Ref, onMounted } from 'vue';
|
||||
import { ref, type Ref, onMounted, computed } from 'vue';
|
||||
import { type LprVendor } from '@/types';
|
||||
import { lprBaseTags } from '@/constants';
|
||||
import { useVendorStore } from '@/stores/vendorStore';
|
||||
@@ -62,10 +56,19 @@ const lprVendors = ref<LprVendor[]>([]);
|
||||
const selectedLprVendor: Ref<LprVendor | null> = ref(null);
|
||||
const vendorStore = useVendorStore();
|
||||
|
||||
function highlightClass(item: LprVendor | null): string {
|
||||
if (!item) return 'placeholder';
|
||||
return item.shortName === 'Generic' ? 'placeholder' : 'highlight';
|
||||
}
|
||||
const operatorTags: Record<string, string> = {
|
||||
'operator': '[Enter operator name]',
|
||||
'operator:wikidata': '[Enter WikiData ID]'
|
||||
};
|
||||
|
||||
const mergedTags = computed(() => ({
|
||||
...lprBaseTags,
|
||||
...(selectedLprVendor.value?.osmTags ?? {})
|
||||
}));
|
||||
|
||||
const vendorTagKeys = computed(() =>
|
||||
Object.keys(selectedLprVendor.value?.osmTags ?? {})
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
const genericLprVendor: LprVendor = {
|
||||
@@ -84,22 +87,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.highlight {
|
||||
background-color: #0081ac;
|
||||
padding: 0.15rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
background-color: #ffe066;
|
||||
color: #333;
|
||||
padding: 0.15rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
121
webapp/src/components/ShareDialog.vue
Normal file
121
webapp/src/components/ShareDialog.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<v-dialog :model-value="modelValue" @update:model-value="$emit('update:modelValue', $event)" max-width="650">
|
||||
<v-card>
|
||||
<v-card-title class="text-h5 font-weight-bold text-center">
|
||||
Share This Map
|
||||
</v-card-title>
|
||||
<v-divider />
|
||||
<v-card-text class="pa-6">
|
||||
<!-- Social Sharing Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-crop-free</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-bold">Link to this Region</h3>
|
||||
</div>
|
||||
<div class="d-flex ga-2 flex-wrap">
|
||||
<v-btn
|
||||
color="var(--df-blue)"
|
||||
class="text-white"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-link-variant"
|
||||
@click="copyToClipboard(shareUrl)"
|
||||
>
|
||||
Copy Link
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:href="redditShareUrl"
|
||||
target="_blank"
|
||||
color="#FF4500"
|
||||
variant="flat"
|
||||
prepend-icon="mdi-reddit"
|
||||
>
|
||||
Reddit
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<!-- Embed Section -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-code-tags</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-bold">Embed on Your Site</h3>
|
||||
</div>
|
||||
<DFCode><iframe src="{{ shareUrl }}" width="100%" height="600" style="border: none;"></iframe></DFCode>
|
||||
</div>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<!-- Download Data Section -->
|
||||
<div>
|
||||
<div class="d-flex align-center mb-3">
|
||||
<v-icon class="mr-2" color="primary">mdi-download</v-icon>
|
||||
<h3 class="text-subtitle-1 font-weight-bold">Download All Data (Coming Soon)</h3>
|
||||
</div>
|
||||
<p class="text-body-2 mb-3">Get the complete dataset in GeoJSON format.</p>
|
||||
<v-btn
|
||||
href="#"
|
||||
target="_blank"
|
||||
download
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="large"
|
||||
block
|
||||
prepend-icon="mdi-download"
|
||||
disabled
|
||||
>
|
||||
Coming Soon
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-divider />
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn @click="$emit('update:modelValue', false)" variant="text">Close</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar v-model="snackbarOpen" :timeout="3000" color="var(--df-blue-dark)">
|
||||
<span class="text-white">Copied to clipboard!</span>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="snackbarOpen = false"
|
||||
color="white"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import DFCode from '@/components/DFCode.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean,
|
||||
}>();
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const router = useRouter();
|
||||
const snackbarOpen = ref(false);
|
||||
|
||||
const shareUrl = computed(() => {
|
||||
return window.location.href;
|
||||
});
|
||||
|
||||
const redditShareUrl = computed(() => {
|
||||
const title = encodeURIComponent('DeFlock - License Plate Readers Near You');
|
||||
return `https://reddit.com/submit?url=${encodeURIComponent(shareUrl.value)}&title=${title}`;
|
||||
});
|
||||
|
||||
function copyToClipboard(text: string) {
|
||||
navigator.clipboard.writeText(text)
|
||||
.then(() => snackbarOpen.value = true)
|
||||
.catch(() => console.error('Failed to copy to clipboard'));
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,10 @@
|
||||
export const lprBaseTags = {
|
||||
"man_made": "surveillance",
|
||||
"surveillance:type": "ALPR",
|
||||
"camera:type": "fixed"
|
||||
"surveillance": "public",
|
||||
"camera:type": "fixed",
|
||||
"surveillance:zone": "traffic",
|
||||
"camera:mount": "",
|
||||
"electricity": "",
|
||||
"note": ""
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LprVendor } from "@/types";
|
||||
import type { LprVendor, OtherSurveillanceDevice } from "@/types";
|
||||
import axios from "axios";
|
||||
|
||||
export interface Chapter {
|
||||
@@ -42,5 +42,14 @@ export const cmsService = {
|
||||
console.error("Error fetching LPR vendors:", error);
|
||||
throw new Error("Failed to fetch LPR vendors");
|
||||
}
|
||||
},
|
||||
async getOtherSurveillanceDevices(): Promise<OtherSurveillanceDevice[]> {
|
||||
try {
|
||||
const response = await cmsApiService.get("/items/nonLprVendors");
|
||||
return response.data.data as OtherSurveillanceDevice[];
|
||||
} catch (error) {
|
||||
console.error("Error fetching other surveillance devices:", error);
|
||||
throw new Error("Failed to fetch other surveillance devices");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { lprBaseTags } from "@/constants";
|
||||
|
||||
interface DeflockProfileTags {
|
||||
interface DeflockProfile {
|
||||
name: string;
|
||||
tags: Record<string, string>;
|
||||
requiresDirection: boolean;
|
||||
submittable: boolean;
|
||||
fov: number;
|
||||
fov: number | null;
|
||||
}
|
||||
|
||||
export function createDeflockProfileUrl(name: string, obj: Record<string, string>): string {
|
||||
const tags = { ...lprBaseTags, ...obj };
|
||||
const profile: DeflockProfileTags = {
|
||||
interface DeflockProfileOptions {
|
||||
requiresDirection?: boolean;
|
||||
fov?: number | null;
|
||||
}
|
||||
|
||||
export function createDeflockProfileUrl(name: string, osmTags: Record<string, string>, options?: DeflockProfileOptions): string {
|
||||
const { requiresDirection = true, fov = null } = options || {};
|
||||
|
||||
const profile: DeflockProfile = {
|
||||
name,
|
||||
tags,
|
||||
requiresDirection: true,
|
||||
tags: osmTags,
|
||||
submittable: true,
|
||||
fov: 90.0,
|
||||
requiresDirection,
|
||||
fov
|
||||
};
|
||||
const payload = btoa(JSON.stringify(profile));
|
||||
return `deflockapp://profiles/add?p=${payload}`;
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { cmsService } from '@/services/cmsService';
|
||||
import type { LprVendor } from '@/types';
|
||||
import type { LprVendor, OtherSurveillanceDevice } from '@/types';
|
||||
|
||||
export const useVendorStore = defineStore('vendorStore', () => {
|
||||
const lprVendors = ref<LprVendor[]>([]);
|
||||
const otherDevices = ref<OtherSurveillanceDevice[]>([]);
|
||||
|
||||
async function loadAllVendors(): Promise<void> {
|
||||
if (lprVendors.value.length > 0) return;
|
||||
lprVendors.value = await cmsService.getLprVendors();
|
||||
}
|
||||
|
||||
async function loadAllOtherDevices(): Promise<void> {
|
||||
if (otherDevices.value.length > 0) return;
|
||||
otherDevices.value = await cmsService.getOtherSurveillanceDevices();
|
||||
}
|
||||
|
||||
async function getFirstImageForManufacturer(fullName: string): Promise<string | null> {
|
||||
const vendor = lprVendors.value.find(v => v.fullName === fullName);
|
||||
return vendor?.urls?.[0]?.url ?? null;
|
||||
}
|
||||
|
||||
return { lprVendors, loadAllVendors, getFirstImageForManufacturer };
|
||||
return { lprVendors, otherDevices, loadAllVendors, loadAllOtherDevices, getFirstImageForManufacturer };
|
||||
});
|
||||
|
||||
@@ -15,3 +15,14 @@ export interface LprVendor {
|
||||
logoUrl?: string;
|
||||
osmTags: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface OtherSurveillanceDevice {
|
||||
id: number;
|
||||
capabilities?: string;
|
||||
category: string;
|
||||
fov?: number;
|
||||
name: string;
|
||||
osmTags: Record<string, string>;
|
||||
requiresDirection: boolean;
|
||||
urls: Array<{ url: string }>;
|
||||
}
|
||||
|
||||
@@ -2,162 +2,248 @@
|
||||
<DefaultLayout>
|
||||
<template #header>
|
||||
<Hero
|
||||
title="How to Identify LPRs"
|
||||
description="Visual guide to identifying license plate readers"
|
||||
title="How to Identify Surveillance Equipment"
|
||||
description="Learn to identify LPRs and other devices"
|
||||
gradient="linear-gradient(135deg, rgb(var(--v-theme-primary)) 0%, rgb(var(--v-theme-secondary)) 100%)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<v-container fluid>
|
||||
<v-container>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel elevation="2">
|
||||
<v-expansion-panel-title class="text-h5 font-weight-bold">
|
||||
Common Features
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list lines="two">
|
||||
<v-list-item prepend-icon="mdi-eye">
|
||||
<v-list-item-title class="text-h6">Rear-Facing Cameras</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
LPRs almost always face the <b>rear of vehicles</b>.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item prepend-icon="mdi-lightbulb-on">
|
||||
<v-list-item-title class="text-h6">Infrared Lights</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Look for infrared lights that emit a <b>faint red glow</b> at night.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item prepend-icon="mdi-solar-panel">
|
||||
<v-list-item-title class="text-h6">Solar Panels</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Many LPRs are powered by nearby <b>solar panels</b>.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<v-tabs
|
||||
v-model="activeTab"
|
||||
align-tabs="center"
|
||||
color="primary"
|
||||
class="mb-6"
|
||||
>
|
||||
<v-tab value="alprs">License Plate Readers</v-tab>
|
||||
<v-tab value="other">Other Devices</v-tab>
|
||||
</v-tabs>
|
||||
</v-container>
|
||||
|
||||
<v-container class="mb-12">
|
||||
<h2 class="text-h4 text-center mt-3">Common LPR Vendors</h2>
|
||||
<p class="text-center text-medium-emphasis mb-8">
|
||||
Most LPRs are easy to recognize.
|
||||
</p>
|
||||
<v-window v-model="activeTab">
|
||||
<!-- ALPR Tab -->
|
||||
<v-window-item value="alprs">
|
||||
<v-container>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel elevation="2">
|
||||
<v-expansion-panel-title class="text-h5 font-weight-bold">
|
||||
Common Features
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-list lines="two">
|
||||
<v-list-item prepend-icon="mdi-eye">
|
||||
<v-list-item-title class="text-h6">Rear-Facing Cameras</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
LPRs almost always face the <b>rear of vehicles</b>.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<!-- Skeleton Loader -->
|
||||
<v-row v-if="loading">
|
||||
<v-col cols="12" md="6" v-for="n in 4" :key="`skeleton-${n}`" class="mb-4">
|
||||
<v-card class="vendor-card h-100" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;">
|
||||
<v-skeleton-loader type="image" style="height:48px; width:150px; margin:0 auto;" />
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-center pa-4">
|
||||
<v-skeleton-loader type="text" width="80%" />
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="pa-4">
|
||||
<v-row>
|
||||
<v-col cols="6" v-for="i in 4" :key="`skeleton-img-${i}`">
|
||||
<v-skeleton-loader type="image" :height="120" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-list-item prepend-icon="mdi-lightbulb-on">
|
||||
<v-list-item-title class="text-h6">Infrared Lights</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Look for infrared lights that emit a <b>faint red glow</b> at night.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col cols="12" md="6" v-for="vendor in vendorStore.lprVendors" :key="vendor.id" class="mb-4">
|
||||
<v-card class="vendor-card h-100" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;"
|
||||
@click="onVendorTitleClick(vendor.id)"
|
||||
>
|
||||
<v-img v-if="vendor.logoUrl" contain :src="vendor.logoUrl" :alt="`${vendor.shortName} Logo`" style="height: 48px;" />
|
||||
<div
|
||||
style="height: 48px; display: flex; align-items: center; justify-content: center;"
|
||||
class="font-weight-bold text-black"
|
||||
v-else
|
||||
>
|
||||
{{ vendor.shortName }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-center pa-4 text-h6" style="white-space: normal; word-break: break-word;">
|
||||
{{ vendor.identificationHints }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="pa-4">
|
||||
<v-list-item prepend-icon="mdi-solar-panel">
|
||||
<v-list-item-title class="text-h6">Solar Panels</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Many LPRs are powered by nearby <b>solar panels</b>.
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
|
||||
<v-container class="mb-12">
|
||||
<h2 class="text-h4 text-center mt-3">Common LPR Vendors</h2>
|
||||
<p class="text-center text-medium-emphasis mb-8">
|
||||
Most LPRs are easy to recognize.
|
||||
</p>
|
||||
|
||||
<!-- Skeleton Loader -->
|
||||
<v-row v-if="loading">
|
||||
<v-col cols="12" md="6" v-for="n in 4" :key="`skeleton-${n}`" class="mb-4">
|
||||
<v-card class="vendor-card h-100" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;">
|
||||
<v-skeleton-loader type="image" style="height:48px; width:150px; margin:0 auto;" />
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-center pa-4">
|
||||
<v-skeleton-loader type="text" width="80%" />
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="pa-4">
|
||||
<v-row>
|
||||
<v-col cols="6" v-for="i in 4" :key="`skeleton-img-${i}`">
|
||||
<v-skeleton-loader type="image" :height="120" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col cols="12" md="6" v-for="vendor in vendorStore.lprVendors" :key="vendor.id" class="mb-4">
|
||||
<v-card class="vendor-card h-100 d-flex flex-column justify-space-between" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;">
|
||||
<v-img v-if="vendor.logoUrl" contain :src="vendor.logoUrl" :alt="`${vendor.shortName} Logo`" style="height: 48px;" />
|
||||
<div
|
||||
style="height: 48px; display: flex; align-items: center; justify-content: center;"
|
||||
class="font-weight-bold text-black"
|
||||
v-else
|
||||
>
|
||||
{{ vendor.shortName }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-subtitle class="text-center pa-4 text-h6" style="white-space: normal; word-break: break-word;">
|
||||
{{ vendor.identificationHints }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="pa-4 d-flex flex-column justify-space-between">
|
||||
<v-row>
|
||||
<v-col v-for="{ url: imageUrl } in vendor.urls" :key="imageUrl" cols="6">
|
||||
<v-card class="image-card" elevation="1" @click="openImageInNewTab(imageUrl)">
|
||||
<v-img
|
||||
:src="imageUrl"
|
||||
:aspect-ratio="4/3"
|
||||
cover
|
||||
class="cursor-pointer"
|
||||
>
|
||||
</v-img>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="text-center mb-2">OSM Tags</h4>
|
||||
<DFCode
|
||||
v-if="hasOsmTags(vendor.osmTags)"
|
||||
:osm-tags="getMergedTags(vendor)"
|
||||
:highlight-values-for-keys="getVendorTagKeys(vendor)"
|
||||
/>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
Coming soon
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<v-btn
|
||||
color="var(--df-blue)"
|
||||
class="text-white mt-3"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
:disabled="!hasOsmTags(vendor.osmTags)"
|
||||
@click="onAddToApp(vendor.shortName, vendor.osmTags, true, true, null)"
|
||||
prepend-icon="mdi-application-import"
|
||||
>
|
||||
Import to App
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="text-center text-caption mt-1">Requires app v2.7.1+</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-window-item>
|
||||
|
||||
<!-- Other Surveillance Tab -->
|
||||
<v-window-item value="other">
|
||||
<v-container class="mb-12">
|
||||
<!-- Skeleton Loader -->
|
||||
<div v-if="loadingOther">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6" v-for="n in 4" :key="`skeleton-other-${n}`" class="mb-4">
|
||||
<v-card class="vendor-card h-100" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;">
|
||||
<v-skeleton-loader type="text" style="height:48px; width:150px; margin:0 auto;" />
|
||||
</v-card-title>
|
||||
<v-card-text class="pa-4">
|
||||
<v-row>
|
||||
<v-col cols="6" v-for="i in 4" :key="`skeleton-other-img-${i}`">
|
||||
<v-skeleton-loader type="image" :height="120" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-for="(devices, category) in devicesByCategory" :key="category" class="mb-8">
|
||||
<h2 class="text-h4 text-center mt-6 mb-6">{{ category }}</h2>
|
||||
|
||||
<v-row>
|
||||
<v-col v-for="{ url: imageUrl } in vendor.urls" :key="imageUrl" cols="6">
|
||||
<v-card class="image-card" elevation="1" @click="openImageInNewTab(imageUrl)">
|
||||
<v-img
|
||||
:src="imageUrl"
|
||||
:aspect-ratio="4/3"
|
||||
cover
|
||||
class="cursor-pointer"
|
||||
>
|
||||
</v-img>
|
||||
<v-col cols="12" md="6" v-for="device in devices" :key="device.id" class="mb-4">
|
||||
<v-card class="vendor-card h-100 d-flex flex-column justify-space-between" elevation="2">
|
||||
<v-card-title class="text-center" style="background-color: #f5f5f5;">
|
||||
<div
|
||||
style="height: 48px; display: flex; align-items: center; justify-content: center;"
|
||||
class="font-weight-bold text-black"
|
||||
>
|
||||
{{ device.name }}
|
||||
</div>
|
||||
</v-card-title>
|
||||
<v-card-subtitle v-if="device.capabilities" class="text-center pa-4 text-h6" style="white-space: normal; word-break: break-word;">
|
||||
{{ device.capabilities }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text class="pa-4 d-flex flex-column justify-space-between">
|
||||
<v-row>
|
||||
<v-col v-for="{ url: imageUrl } in device.urls" :key="imageUrl" cols="6">
|
||||
<v-card class="image-card" elevation="1" @click="openImageInNewTab(imageUrl)">
|
||||
<v-img
|
||||
:src="imageUrl"
|
||||
:aspect-ratio="4/3"
|
||||
cover
|
||||
class="cursor-pointer"
|
||||
>
|
||||
</v-img>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4" />
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="text-center mb-2">OSM Tags</h4>
|
||||
<DFCode
|
||||
v-if="hasOsmTags(device.osmTags)"
|
||||
:osm-tags="device.osmTags"
|
||||
/>
|
||||
<div v-else class="text-center pa-4 text-medium-emphasis">
|
||||
Coming soon
|
||||
</div>
|
||||
<div class="text-center mt-3">
|
||||
<v-btn
|
||||
color="var(--df-blue)"
|
||||
class="text-white mt-3"
|
||||
variant="elevated"
|
||||
size="large"
|
||||
:disabled="!hasOsmTags(device.osmTags)"
|
||||
@click="onAddToApp(device.name, device.osmTags, false, device.requiresDirection, device.fov ?? null)"
|
||||
prepend-icon="mdi-application-import"
|
||||
>
|
||||
Import to App
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="text-center text-caption text-grey-darken-1 mt-1">Requires app v2.7.1+</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<!-- What ALPRs are NOT -->
|
||||
<v-container class="mb-12">
|
||||
<v-card class="not-alpr-card" elevation="3">
|
||||
<v-card-title
|
||||
class="text-center px-6 bg-warning font-weight-bold"
|
||||
:class="[$vuetify.display.smAndDown ? 'text-h6' : 'text-h4']"
|
||||
style="white-space: normal; word-break: break-word;"
|
||||
>
|
||||
NOT ALPRs
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="px-6 pt-6">
|
||||
<v-row>
|
||||
<!-- Traffic Detection Cameras -->
|
||||
<v-col cols="12" md="9" class="mb-6">
|
||||
<h3 class="text-center mb-4">Traffic Detection Cameras</h3>
|
||||
<v-row>
|
||||
<v-col v-for="(image, index) in trafficCameraImages" :key="index" cols="12" md="4">
|
||||
<v-card class="image-card" elevation="1" @click="openImageInNewTab(image)">
|
||||
<v-img
|
||||
:src="image"
|
||||
:aspect-ratio="4/3"
|
||||
cover
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
|
||||
<!-- Snow Detection Cameras -->
|
||||
<v-col cols="12" md="3" class="mb-6">
|
||||
<h3 class="text-center mb-4">Snow/Ice Cameras</h3>
|
||||
<v-row>
|
||||
<v-col v-for="(image, index) in snowDetectionImages" :key="index" cols="12">
|
||||
<v-card class="image-card" elevation="1" @click="openImageInNewTab(image)">
|
||||
<v-img
|
||||
:src="image"
|
||||
:aspect-ratio="4/3"
|
||||
cover
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</div>
|
||||
<div class="text-center text-body-1">
|
||||
Don't see a device you know about? We're always adding more, so check back later!
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
||||
<!-- Action Section -->
|
||||
<v-container class="text-center mb-12">
|
||||
@@ -166,11 +252,11 @@
|
||||
<v-card-text>
|
||||
<v-btn
|
||||
size="x-large"
|
||||
color="primary"
|
||||
color="var(--df-blue)"
|
||||
to="/report"
|
||||
prepend-icon="mdi-map-marker-plus"
|
||||
variant="elevated"
|
||||
class="mr-4"
|
||||
class="text-white"
|
||||
>
|
||||
Add to Map
|
||||
</v-btn>
|
||||
@@ -184,74 +270,92 @@
|
||||
<script setup lang="ts">
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue';
|
||||
import Hero from '@/components/layout/Hero.vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import DFCode from '@/components/DFCode.vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useVendorStore } from '@/stores/vendorStore';
|
||||
import { createDeflockProfileUrl } from '@/services/deflockAppUrls';
|
||||
import type { LprVendor } from '@/types';
|
||||
import { lprBaseTags } from '@/constants';
|
||||
import type { LprVendor, OtherSurveillanceDevice } from '@/types';
|
||||
|
||||
function openImageInNewTab(url: string) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
const loading = ref(true);
|
||||
const vendorStore = useVendorStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// BEGIN SECRET CLICKS
|
||||
const secretClicks = new Map<string | number, number[]>();
|
||||
|
||||
function onVendorTitleClick(vendorId: string | number) {
|
||||
const now = Date.now();
|
||||
const windowMs = 2500; // 2.5 seconds
|
||||
const required = 3;
|
||||
const arr = secretClicks.get(vendorId) || [];
|
||||
// keep clicks within window
|
||||
const filtered = arr.filter(t => now - t <= windowMs);
|
||||
filtered.push(now);
|
||||
secretClicks.set(vendorId, filtered);
|
||||
if (filtered.length >= required) {
|
||||
// find vendor object from store and call onAddToApp
|
||||
const vendor = vendorStore.lprVendors.find(v => v.id === vendorId);
|
||||
if (vendor) onAddToApp(vendor as any);
|
||||
secretClicks.set(vendorId, []);
|
||||
const activeTab = computed({
|
||||
get: () => {
|
||||
const tab = route.query.tab as string;
|
||||
return tab === 'other' ? 'other' : 'alprs';
|
||||
},
|
||||
set: (tab: string) => {
|
||||
router.push({
|
||||
path: route.path,
|
||||
query: { ...route.query, tab: tab === 'alprs' ? undefined : tab }
|
||||
});
|
||||
}
|
||||
}
|
||||
// END SECRET CLICKS
|
||||
|
||||
onMounted(async () => {
|
||||
await vendorStore.loadAllVendors();
|
||||
loading.value = false;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const loading = ref(true);
|
||||
const loadingOther = ref(true);
|
||||
const vendorStore = useVendorStore();
|
||||
|
||||
async function onAddToApp(vendor: LprVendor) {
|
||||
const url = createDeflockProfileUrl(vendor.shortName, vendor.osmTags);
|
||||
function getMergedTags(vendor: LprVendor): Record<string, string> {
|
||||
return { ...lprBaseTags, ...vendor.osmTags };
|
||||
}
|
||||
|
||||
function getVendorTagKeys(vendor: LprVendor): string[] {
|
||||
return Object.keys(vendor.osmTags ?? {});
|
||||
}
|
||||
|
||||
function hasOsmTags(osmTags: Record<string, string> | undefined): boolean {
|
||||
return osmTags !== undefined && Object.keys(osmTags).length > 0;
|
||||
}
|
||||
|
||||
// Group other surveillance devices by category, preserving CMS order
|
||||
const devicesByCategory = computed(() => {
|
||||
const grouped: Record<string, OtherSurveillanceDevice[]> = {};
|
||||
|
||||
for (const device of vendorStore.otherDevices) {
|
||||
if (!grouped[device.category]) {
|
||||
grouped[device.category] = [];
|
||||
}
|
||||
grouped[device.category].push(device);
|
||||
}
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
vendorStore.loadAllVendors().then(() => { loading.value = false; }),
|
||||
vendorStore.loadAllOtherDevices().then(() => { loadingOther.value = false; })
|
||||
]);
|
||||
});
|
||||
|
||||
async function onAddToApp(
|
||||
name: string,
|
||||
osmTags: Record<string, string>,
|
||||
isAlpr: boolean,
|
||||
requiresDirection: boolean,
|
||||
fov: number | null
|
||||
) {
|
||||
const tags = isAlpr ? { ...lprBaseTags, ...osmTags } : osmTags;
|
||||
const url = createDeflockProfileUrl(name, tags, { requiresDirection, fov });
|
||||
const ua = typeof navigator !== 'undefined' && navigator.userAgent ? navigator.userAgent : '';
|
||||
const isMobile = /iphone|ipod|ipad|android|blackberry|bb|playbook|windows phone|iemobile|opera mini|mobile/i.test(ua);
|
||||
if (isMobile) {
|
||||
// attempt to open the app via custom scheme on mobile
|
||||
try {
|
||||
window.location.href = url;
|
||||
} catch (e) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
} else {
|
||||
// on Desktop
|
||||
console.log(`Deflock profile URL: ${url}`);
|
||||
// router.push('/app');
|
||||
router.push('/app');
|
||||
}
|
||||
}
|
||||
|
||||
const trafficCameraImages = [
|
||||
'/non-alprs/iteris.webp',
|
||||
'/non-alprs/traffic-cam.webp',
|
||||
'/non-alprs/flir.webp',
|
||||
];
|
||||
|
||||
const snowDetectionImages = [
|
||||
'/non-alprs/frost-cam.jpeg'
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -263,10 +367,6 @@ const snowDetectionImages = [
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.vendor-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.image-card {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<NewVisitor />
|
||||
<NewVisitor v-if="!isIframe" />
|
||||
<ShareDialog v-model="shareDialogOpen" />
|
||||
|
||||
<div class="map-container" @keyup="handleKeyUp">
|
||||
<leaflet-map
|
||||
v-if="center"
|
||||
@@ -36,8 +38,13 @@
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<!-- CURRENT LOCATION -->
|
||||
<template v-slot:bottomright>
|
||||
<v-btn icon @click="shareDialogOpen = true" v-if="!isIframe">
|
||||
<v-icon>mdi-share-variant</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon to="/report" style="color: unset" v-if="!isIframe">
|
||||
<v-icon size="large">mdi-map-marker-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon @click="goToUserLocation">
|
||||
<v-icon>mdi-crosshairs-gps</v-icon>
|
||||
</v-btn>
|
||||
@@ -52,12 +59,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { Ref } from 'vue';
|
||||
import { BoundingBox } from '@/services/apiService';
|
||||
import { geocodeQuery } from '@/services/apiService';
|
||||
import { useDisplay, useTheme } from 'vuetify';
|
||||
import { useDisplay } from 'vuetify';
|
||||
import { useGlobalStore } from '@/stores/global';
|
||||
import { useTilesStore } from '@/stores/tiles';
|
||||
import { useVendorStore } from '@/stores/vendorStore';
|
||||
@@ -66,6 +73,7 @@ globalThis.L = L;
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import LeafletMap from '@/components/LeafletMap.vue';
|
||||
import NewVisitor from '@/components/NewVisitor.vue';
|
||||
import ShareDialog from '@/components/ShareDialog.vue';
|
||||
|
||||
const DEFAULT_ZOOM = 12;
|
||||
|
||||
@@ -76,8 +84,11 @@ const searchField: Ref<any|null> = ref(null);
|
||||
const searchInput: Ref<string> = ref(''); // For the text input field
|
||||
const searchQuery: Ref<string> = ref(''); // For URL and boundaries (persistent)
|
||||
const geojson: Ref<GeoJSON.GeoJsonObject | null> = ref(null);
|
||||
const shareDialogOpen = ref(false);
|
||||
const tilesStore = useTilesStore();
|
||||
|
||||
const isIframe = computed(() => window.self !== window.top);
|
||||
|
||||
const { fetchVisibleTiles } = tilesStore;
|
||||
const alprs = computed(() => tilesStore.allNodes);
|
||||
|
||||
@@ -101,7 +112,7 @@ function onSearch() {
|
||||
if (!searchInput.value) {
|
||||
return;
|
||||
}
|
||||
geocodeQuery(searchInput.value, center.value)
|
||||
geocodeQuery(searchInput.value)
|
||||
.then((result: any) => {
|
||||
if (!result) {
|
||||
alert('No results found');
|
||||
|
||||
@@ -41,13 +41,6 @@
|
||||
If you would like to <b>localize the URL</b> to a specific region, please zoom to the area at <router-link to="/map">https://deflock.org/map</router-link> and copy the URL from your browser's address bar.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
To <b>remove the header bar</b>, add the following query parameter to the URL: <code>?fullscreen=true</code>. For example:
|
||||
<DFCode>
|
||||
<iframe src="https://deflock.org/map?fullscreen=true#map=14/40.014863/-105.266275" width="100%" height="600" style="border: none;"></iframe>
|
||||
</DFCode>
|
||||
</p>
|
||||
|
||||
<h2>Contact Us</h2>
|
||||
<p>
|
||||
For media inquiries and interview requests, send us an email at <a href="mailto:media@deflock.me">media@deflock.me</a>.
|
||||
|
||||
Reference in New Issue
Block a user