mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 16:52:31 +00:00
street view control
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
import Map from '$lib/components/Map.svelte';
|
||||
import Menu from '$lib/components/Menu.svelte';
|
||||
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
|
||||
import StreetViewControl from '$lib/components/street-view-control/StreetViewControl.svelte';
|
||||
import LayerControl from '$lib/components/layer-control/LayerControl.svelte';
|
||||
import { Toaster } from '$lib/components/ui/sonner';
|
||||
|
||||
@@ -20,6 +21,7 @@
|
||||
<Menu />
|
||||
<Toolbar />
|
||||
<Map class="h-full {$verticalFileView ? '' : 'horizontal'}" />
|
||||
<StreetViewControl />
|
||||
<LayerControl />
|
||||
<GPXLayers />
|
||||
<Toaster richColors />
|
||||
|
@@ -32,7 +32,8 @@
|
||||
File,
|
||||
View,
|
||||
FilePen,
|
||||
HeartHandshake
|
||||
HeartHandshake,
|
||||
PersonStanding
|
||||
} from 'lucide-svelte';
|
||||
|
||||
import {
|
||||
@@ -77,7 +78,8 @@
|
||||
currentOverlays,
|
||||
previousOverlays,
|
||||
distanceMarkers,
|
||||
directionMarkers
|
||||
directionMarkers,
|
||||
streetViewSource
|
||||
} = settings;
|
||||
|
||||
$: if ($mode === 'system') {
|
||||
@@ -326,6 +328,18 @@
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
<Menubar.Separator />
|
||||
<Menubar.Sub>
|
||||
<Menubar.SubTrigger>
|
||||
<PersonStanding size="16" class="mr-1" />
|
||||
{$_('menu.street_view_source')}
|
||||
</Menubar.SubTrigger>
|
||||
<Menubar.SubContent>
|
||||
<Menubar.RadioGroup bind:value={$streetViewSource}>
|
||||
<Menubar.RadioItem value="mapillary">{$_('menu.mapillary')}</Menubar.RadioItem>
|
||||
<Menubar.RadioItem value="google">{$_('menu.google')}</Menubar.RadioItem>
|
||||
</Menubar.RadioGroup>
|
||||
</Menubar.SubContent>
|
||||
</Menubar.Sub>
|
||||
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
|
||||
<Layers3 size="16" class="mr-1" />
|
||||
{$_('menu.layers')}
|
||||
|
@@ -1,3 +1,15 @@
|
||||
<script lang="ts" context="module">
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let currentId = 0;
|
||||
|
||||
function getId() {
|
||||
return currentId++;
|
||||
}
|
||||
|
||||
let lastInitializedId = writable(-1);
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import CustomControl from './CustomControl';
|
||||
|
||||
@@ -6,20 +18,22 @@
|
||||
export let position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' = 'top-right';
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
let id = getId();
|
||||
|
||||
$: if ($map && container) {
|
||||
$: if ($map && container && $lastInitializedId + 1 === id) {
|
||||
if (position.includes('right')) container.classList.add('float-right');
|
||||
else container.classList.add('float-left');
|
||||
container.classList.remove('hidden');
|
||||
let control = new CustomControl(container);
|
||||
$map.addControl(control, position);
|
||||
lastInitializedId.set(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={container}
|
||||
class="{$$props.class ||
|
||||
''} clear-both translate-0 m-[10px] pointer-events-auto bg-background rounded shadow-md hidden"
|
||||
''} clear-both translate-0 m-[10px] mb-0 pointer-events-auto bg-background rounded shadow-md hidden"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
33
website/src/lib/components/street-view-control/Google.ts
Normal file
33
website/src/lib/components/street-view-control/Google.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { resetCursor, setCrosshairCursor } from "$lib/utils";
|
||||
import type mapboxgl from "mapbox-gl";
|
||||
|
||||
export class GoogleRedirect {
|
||||
map: mapboxgl.Map;
|
||||
enabled = false;
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
}
|
||||
|
||||
add() {
|
||||
if (this.enabled) return;
|
||||
|
||||
this.enabled = true;
|
||||
setCrosshairCursor();
|
||||
this.map.on('click', this.openStreetView);
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (!this.enabled) return;
|
||||
|
||||
this.enabled = false;
|
||||
resetCursor();
|
||||
this.map.off('click', this.openStreetView);
|
||||
}
|
||||
|
||||
openStreetView(e) {
|
||||
window.open(
|
||||
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
|
||||
);
|
||||
}
|
||||
}
|
123
website/src/lib/components/street-view-control/Mapillary.ts
Normal file
123
website/src/lib/components/street-view-control/Mapillary.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import mapboxgl from "mapbox-gl";
|
||||
import { Viewer } from 'mapillary-js/dist/mapillary.module';
|
||||
import 'mapillary-js/dist/mapillary.css';
|
||||
import { t } from "svelte-i18n";
|
||||
import { resetCursor, setPointerCursor } from "$lib/utils";
|
||||
|
||||
const mapillarySource = {
|
||||
type: 'vector',
|
||||
tiles: ['https://tiles.mapillary.com/maps/vtp/mly1_computed_public/2/{z}/{x}/{y}?access_token=MLY|4381405525255083|3204871ec181638c3c31320490f03011'],
|
||||
minzoom: 6,
|
||||
maxzoom: 14,
|
||||
};
|
||||
|
||||
const mapillarySequenceLayer = {
|
||||
id: 'mapillary-sequence',
|
||||
type: 'line',
|
||||
source: 'mapillary',
|
||||
'source-layer': 'sequence',
|
||||
paint: {
|
||||
'line-color': 'rgb(53, 175, 109)',
|
||||
'line-opacity': 0.5,
|
||||
'line-width': 4,
|
||||
},
|
||||
layout: {
|
||||
'line-cap': 'round',
|
||||
'line-join': 'round',
|
||||
},
|
||||
};
|
||||
|
||||
const mapillaryImageLayer = {
|
||||
id: 'mapillary-image',
|
||||
type: 'circle',
|
||||
source: 'mapillary',
|
||||
'source-layer': 'image',
|
||||
paint: {
|
||||
'circle-color': 'rgb(53, 175, 109)',
|
||||
'circle-radius': 4,
|
||||
'circle-opacity': 0.5,
|
||||
},
|
||||
};
|
||||
|
||||
export class MapillaryLayer {
|
||||
map: mapboxgl.Map;
|
||||
popup: mapboxgl.Popup;
|
||||
viewer: Viewer;
|
||||
addBinded = this.add.bind(this);
|
||||
onMouseEnterBinded = this.onMouseEnter.bind(this);
|
||||
onMouseLeaveBinded = this.onMouseLeave.bind(this);
|
||||
|
||||
constructor(map: mapboxgl.Map) {
|
||||
this.map = map;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.style.width = '400px';
|
||||
container.style.height = '300px';
|
||||
container.className = 'rounded-md border-background border-2'
|
||||
|
||||
this.viewer = new Viewer({
|
||||
accessToken: 'MLY|4381405525255083|3204871ec181638c3c31320490f03011',
|
||||
container,
|
||||
});
|
||||
|
||||
this.popup = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
maxWidth: container.style.width,
|
||||
}).setDOMContent(container);
|
||||
|
||||
this.viewer.on('position', async () => {
|
||||
if (this.popup.isOpen()) {
|
||||
let latLng = await this.viewer.getPosition();
|
||||
this.popup.setLngLat(latLng);
|
||||
if (!this.map.getBounds().contains(latLng)) {
|
||||
this.map.panTo(latLng);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add() {
|
||||
if (!this.map.getSource('mapillary')) {
|
||||
this.map.addSource('mapillary', mapillarySource);
|
||||
}
|
||||
if (!this.map.getLayer('mapillary-sequence')) {
|
||||
this.map.addLayer(mapillarySequenceLayer);
|
||||
}
|
||||
if (!this.map.getLayer('mapillary-image')) {
|
||||
this.map.addLayer(mapillaryImageLayer);
|
||||
}
|
||||
this.map.on('style.load', this.addBinded);
|
||||
this.map.on('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||
this.map.on('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.map.off('style.load', this.addBinded);
|
||||
this.map.off('mouseenter', 'mapillary-image', this.onMouseEnterBinded);
|
||||
this.map.off('mouseleave', 'mapillary-image', this.onMouseLeaveBinded);
|
||||
|
||||
if (this.map.getLayer('mapillary-image')) {
|
||||
this.map.removeLayer('mapillary-image');
|
||||
}
|
||||
if (this.map.getLayer('mapillary-sequence')) {
|
||||
this.map.removeLayer('mapillary-sequence');
|
||||
}
|
||||
if (this.map.getSource('mapillary')) {
|
||||
this.map.removeSource('mapillary');
|
||||
}
|
||||
|
||||
this.popup.remove();
|
||||
}
|
||||
|
||||
onMouseEnter(e: mapboxgl.MapLayerMouseEvent) {
|
||||
this.popup.addTo(this.map).setLngLat(e.lngLat);
|
||||
this.viewer.resize();
|
||||
this.viewer.moveTo(e.features[0].properties.id);
|
||||
|
||||
setPointerCursor();
|
||||
}
|
||||
|
||||
onMouseLeave() {
|
||||
resetCursor();
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||
import { Toggle } from '$lib/components/ui/toggle';
|
||||
import { PersonStanding } from 'lucide-svelte';
|
||||
import { MapillaryLayer } from './Mapillary';
|
||||
import { GoogleRedirect } from './Google';
|
||||
import { map, streetViewEnabled } from '$lib/stores';
|
||||
import { settings } from '$lib/db';
|
||||
|
||||
const { streetViewSource } = settings;
|
||||
|
||||
let googleRedirect: GoogleRedirect;
|
||||
let mapillaryLayer: MapillaryLayer;
|
||||
|
||||
$: if ($map) {
|
||||
googleRedirect = new GoogleRedirect($map);
|
||||
mapillaryLayer = new MapillaryLayer($map);
|
||||
}
|
||||
|
||||
$: if (mapillaryLayer) {
|
||||
if ($streetViewSource === 'mapillary') {
|
||||
googleRedirect.remove();
|
||||
if ($streetViewEnabled) {
|
||||
mapillaryLayer.add();
|
||||
} else {
|
||||
mapillaryLayer.remove();
|
||||
}
|
||||
} else {
|
||||
mapillaryLayer.remove();
|
||||
if ($streetViewEnabled) {
|
||||
googleRedirect.add();
|
||||
} else {
|
||||
googleRedirect.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<CustomControl class="w-[29px] h-[29px]">
|
||||
<Toggle bind:pressed={$streetViewEnabled} class="w-full h-full rounded p-0">
|
||||
<PersonStanding size="22" />
|
||||
</Toggle>
|
||||
</CustomControl>
|
@@ -10,7 +10,7 @@ import { _ } from "svelte-i18n";
|
||||
import { dbUtils, type GPXFileWithStatistics } from "$lib/db";
|
||||
import { selection } from "$lib/components/file-list/Selection";
|
||||
import { ListFileItem, ListTrackItem, ListTrackSegmentItem } from "$lib/components/file-list/FileList";
|
||||
import { currentTool, Tool } from "$lib/stores";
|
||||
import { currentTool, streetViewEnabled, Tool } from "$lib/stores";
|
||||
import { resetCursor, setCrosshairCursor, setGrabbingCursor } from "$lib/utils";
|
||||
|
||||
export const canChangeStart = writable(false);
|
||||
@@ -227,6 +227,10 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
showTemporaryAnchor(e: any) {
|
||||
if (get(streetViewEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!get(selection).hasAnyParent(new ListTrackSegmentItem(this.fileId, e.features[0].properties.trackIndex, e.features[0].properties.segmentIndex))) {
|
||||
return;
|
||||
}
|
||||
@@ -376,6 +380,10 @@ export class RoutingControls {
|
||||
}
|
||||
|
||||
async appendAnchor(e: mapboxgl.MapMouseEvent) { // Add a new anchor to the end of the last segment
|
||||
if (get(streetViewEnabled)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appendAnchorWithCoordinates({
|
||||
lat: e.lngLat.lat,
|
||||
lon: e.lngLat.lng
|
||||
|
@@ -101,6 +101,7 @@ export const settings = {
|
||||
directionMarkers: dexieSettingStore('directionMarkers', false),
|
||||
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
||||
stravaHeatmapColor: dexieSettingStore('stravaHeatmapColor', 'bluered'),
|
||||
streetViewSource: dexieSettingStore('streetViewSource', 'mapillary'),
|
||||
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
|
||||
defaultOpacity: dexieSettingStore('defaultOpacity', 0.7),
|
||||
defaultWeight: dexieSettingStore('defaultWeight', 5),
|
||||
|
@@ -132,6 +132,7 @@ export enum Tool {
|
||||
}
|
||||
export const currentTool = writable<Tool | null>(null);
|
||||
export const splitAs = writable(SplitType.FILES);
|
||||
export const streetViewEnabled = writable(false);
|
||||
|
||||
export function newGPXFile() {
|
||||
const newFileName = get(_)("menu.new_file");
|
||||
|
Reference in New Issue
Block a user