<script lang="ts"> import { Store, UIEventSource } from "../Logic/UIEventSource" import { Map as MlMap } from "maplibre-gl" import MaplibreMap from "./Map/MaplibreMap.svelte" import FeatureSwitchState from "../Logic/State/FeatureSwitchState" import MapControlButton from "./Base/MapControlButton.svelte" import ToSvelte from "./Base/ToSvelte.svelte" import If from "./Base/If.svelte" import type { Feature } from "geojson" import SelectedElementView from "./BigComponents/SelectedElementView.svelte" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import ThemeViewState from "../Models/ThemeViewState" import type { MapProperties } from "../Models/MapProperties" import Geosearch from "./BigComponents/Geosearch.svelte" import Translations from "./i18n/Translations" import { CogIcon, EyeIcon, HeartIcon, MenuIcon, XCircleIcon } from "@rgossiaux/svelte-heroicons/solid" import Tr from "./Base/Tr.svelte" import CommunityIndexView from "./BigComponents/CommunityIndexView.svelte" import FloatOver from "./Base/FloatOver.svelte" import Constants from "../Models/Constants" import TabbedGroup from "./Base/TabbedGroup.svelte" import UserRelatedState from "../Logic/State/UserRelatedState" import LoginToggle from "./Base/LoginToggle.svelte" import LoginButton from "./Base/LoginButton.svelte" import CopyrightPanel from "./BigComponents/CopyrightPanel" import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte" import ModalRight from "./Base/ModalRight.svelte" import { Utils } from "../Utils" import Hotkeys from "./Base/Hotkeys" import LevelSelector from "./BigComponents/LevelSelector.svelte" import ExtraLinkButton from "./BigComponents/ExtraLinkButton" import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte" import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte" import type { RasterLayerPolygon } from "../Models/RasterLayers" import { AvailableRasterLayers } from "../Models/RasterLayers" import RasterLayerOverview from "./Map/RasterLayerOverview.svelte" import IfHidden from "./Base/IfHidden.svelte" import { onDestroy } from "svelte" import MapillaryLink from "./BigComponents/MapillaryLink.svelte" import OpenIdEditor from "./BigComponents/OpenIdEditor.svelte" import OpenBackgroundSelectorButton from "./BigComponents/OpenBackgroundSelectorButton.svelte" import StateIndicator from "./BigComponents/StateIndicator.svelte" import ShareScreen from "./BigComponents/ShareScreen.svelte" import UploadingImageCounter from "./Image/UploadingImageCounter.svelte" import PendingChangesIndicator from "./BigComponents/PendingChangesIndicator.svelte" import Cross from "../assets/svg/Cross.svelte" import LanguagePicker from "./InputElement/LanguagePicker.svelte" import Mastodon from "../assets/svg/Mastodon.svelte" import Bug from "../assets/svg/Bug.svelte" import Liberapay from "../assets/svg/Liberapay.svelte" import OpenJosm from "./Base/OpenJosm.svelte" import Min from "../assets/svg/Min.svelte" import Plus from "../assets/svg/Plus.svelte" import Filter from "../assets/svg/Filter.svelte" import Add from "../assets/svg/Add.svelte" import Community from "../assets/svg/Community.svelte" import Download from "../assets/svg/Download.svelte" import Share from "../assets/svg/Share.svelte" import Favourites from "./Favourites/Favourites.svelte" import ImageOperations from "./Image/ImageOperations.svelte" import VisualFeedbackPanel from "./BigComponents/VisualFeedbackPanel.svelte" import { Orientation } from "../Sensors/Orientation" import GeolocationControl from "./BigComponents/GeolocationControl.svelte" import Compass_arrow from "../assets/svg/Compass_arrow.svelte" import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte" import FilterPanel from "./BigComponents/FilterPanel.svelte" import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte" import { BBox } from "../Logic/BBox" export let state: ThemeViewState let layout = state.layout let maplibremap: UIEventSource<MlMap> = state.map let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined) selectedElement.addCallbackAndRun(se => console.log("Selected element", se)) let compass = Orientation.singleton.alpha let compassLoaded = Orientation.singleton.gotMeasurement Orientation.singleton.startMeasurements() state.selectedElement.addCallback((selected) => { if (!selected) { selectedElement.setData(selected) return } if (selected !== selectedElement.data) { // We first set the selected element to 'undefined' to force the popup to close... selectedElement.setData(undefined) } // ... we give svelte some time to update with requestAnimationFrame ... window.requestAnimationFrame(() => { // ... and we force a fresh popup window selectedElement.setData(selected) }) }) let selectedLayer: Store<LayerConfig> = state.selectedElement.mapD((element) => { if (element.properties.id.startsWith("current_view")) { return currentViewLayer } if(element.properties.id === "location_track"){ return layout.layers.find(l => l.id === "gps_track") } return state.layout.getMatchingLayer(element.properties) }, ) let currentZoom = state.mapProperties.zoom let showCrosshair = state.userRelatedState.showCrosshair let visualFeedback = state.visualFeedback let viewport: UIEventSource<HTMLDivElement> = new UIEventSource<HTMLDivElement>(undefined) let mapproperties: MapProperties = state.mapProperties state.mapProperties.installCustomKeyboardHandler(viewport) function updateViewport() { const rect = viewport.data?.getBoundingClientRect() if (!rect) { return } const mlmap = state.map.data if (!mlmap) { return undefined } const topLeft = mlmap.unproject([rect.left, rect.top]) const bottomRight = mlmap.unproject([rect.right, rect.bottom]) const bbox = new BBox([ [topLeft.lng, topLeft.lat], [bottomRight.lng, bottomRight.lat], ]) state.visualFeedbackViewportBounds.setData(bbox) } viewport.addCallbackAndRunD((_) => { updateViewport() }) mapproperties.bounds.addCallbackAndRunD((_) => { updateViewport() }) let featureSwitches: FeatureSwitchState = state.featureSwitches let availableLayers = state.availableLayers let currentViewLayer = layout.layers.find((l) => l.id === "current_view") let rasterLayer: Store<RasterLayerPolygon> = state.mapProperties.rasterLayer let rasterLayerName = rasterLayer.data?.properties?.name ?? AvailableRasterLayers.maptilerDefaultLayer.properties.name onDestroy( rasterLayer.addCallbackAndRunD((l) => { rasterLayerName = l.properties.name }), ) let previewedImage = state.previewedImage let debug = state.featureSwitches.featureSwitchIsDebugging debug.addCallbackAndRun((dbg) => { if (dbg) { document.body.classList.add("debug") } else { document.body.classList.remove("debug") } }) function forwardEventToMap(e: KeyboardEvent) { const mlmap = state.map.data if (!mlmap) { return } if (!mlmap.keyboard.isEnabled()) { return } const animation = mlmap.keyboard?.keydown(e) animation?.cameraAnimation(mlmap) } </script> <div class="absolute top-0 left-0 h-screen w-screen overflow-hidden"> <MaplibreMap map={maplibremap} /> </div> {#if $visualFeedback} <div class="pointer-events-none absolute top-0 left-0 flex h-screen w-screen items-center justify-center overflow-hidden" > <div bind:this={$viewport} class:border={$visualFeedback} style="border: 2px solid #ff000044; width: 300px; height: 300px" /> </div> {/if} <div class="pointer-events-none absolute top-0 left-0 w-full"> <!-- Top components --> <div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2"> <If condition={state.visualFeedback}> {#if $selectedElement === undefined} <div class="w-fit"> <VisualFeedbackPanel {state} /> </div> {/if} </If> <If condition={state.featureSwitches.featureSwitchSearch}> <Geosearch bounds={state.mapProperties.bounds} on:searchCompleted={() => { state.map?.data?.getCanvas()?.focus() }} perLayer={state.perLayer} selectedElement={state.selectedElement} geolocationState={state.geolocation.geolocationState} /> </If> </div> <div class="float-left m-1 flex flex-col sm:mt-2"> <MapControlButton on:click={() => state.guistate.themeIsOpened.setData(true)} on:keydown={forwardEventToMap} > <div class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 md:mx-2"> <img class="mr-0.5 block h-6 w-6 sm:mr-1 md:mr-2 md:h-8 md:w-8" src={layout.icon} /> <b class="mr-1"> <Tr t={layout.title} /> </b> </div> </MapControlButton> <MapControlButton arialabel={Translations.t.general.labels.menu} on:click={() => state.guistate.menuIsOpened.setData(true)} on:keydown={forwardEventToMap} > <MenuIcon class="h-8 w-8 cursor-pointer" /> </MapControlButton> {#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()} <MapControlButton on:click={() => { state.selectedElement.setData(state.currentView.features?.data?.[0]) }} on:keydown={forwardEventToMap} > <ToSvelte construct={() => currentViewLayer.defaultIcon().SetClass("w-8 h-8 cursor-pointer")} /> </MapControlButton> {/if} <ToSvelte construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")} /> <UploadingImageCounter featureId="*" showThankYou={false} {state} /> <PendingChangesIndicator {state} /> <If condition={state.featureSwitchIsTesting}> <div class="alert w-fit">Testmode</div> </If> </div> <div class="flex w-full flex-col items-center justify-center"> <!-- Flex and w-full are needed for the positioning --> <!-- Centermessage --> <StateIndicator {state} /> <ReverseGeocoding {state} /> </div> </div> <div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen"> <!-- bottom controls --> <div class="flex w-full items-end justify-between px-4"> <div class="flex flex-col"> <If condition={featureSwitches.featureSwitchEnableLogin}> {#if state.lastClickObject.hasPresets || state.lastClickObject.hasNoteLayer} <button class="pointer-events-auto w-fit" on:click={() => { state.openNewDialog() }} on:keydown={forwardEventToMap} > {#if state.lastClickObject.hasPresets} <Tr t={Translations.t.general.add.title} /> {:else} <Tr t={Translations.t.notes.addAComment} /> {/if} </button> {/if} </If> <div class="flex"> <!-- bottom left elements --> <If condition={state.featureSwitches.featureSwitchFilter}> <MapControlButton arialabel={Translations.t.general.labels.filter} on:click={() => state.guistate.openFilterView()} on:keydown={forwardEventToMap} > <Filter class="h-6 w-6" /> </MapControlButton> </If> <If condition={state.featureSwitches.featureSwitchBackgroundSelection}> <OpenBackgroundSelectorButton hideTooltip={true} {state} /> </If> <a class="bg-black-transparent pointer-events-auto h-fit max-h-12 cursor-pointer self-end overflow-hidden rounded-2xl pl-1 pr-2 text-white opacity-50 hover:opacity-100" on:click={() => { state.guistate.themeViewTab.setData("copyright") state.guistate.themeIsOpened.setData(true) }} > © OpenStreetMap, <span class="w-24">{rasterLayerName}</span> </a> </div> </div> <div class="flex flex-col items-end"> <!-- bottom right elements --> <If condition={state.floors.map((f) => f.length > 1)}> <div class="pointer-events-auto mr-0.5"> <LevelSelector floors={state.floors} layerState={state.layerState} zoom={state.mapProperties.zoom} /> </div> </If> <MapControlButton arialabel={Translations.t.general.labels.zoomIn} on:click={() => mapproperties.zoom.update((z) => z + 1)} on:keydown={forwardEventToMap} > <Plus class="h-8 w-8" /> </MapControlButton> <MapControlButton arialabel={Translations.t.general.labels.zoomOut} on:click={() => mapproperties.zoom.update((z) => z - 1)} on:keydown={forwardEventToMap} > <Min class="h-8 w-8" /> </MapControlButton> <If condition={featureSwitches.featureSwitchGeolocation}> <div class="relative m-0"> <MapControlButton arialabel={Translations.t.general.labels.jumpToLocation} on:click={() => state.geolocationControl.handleClick()} on:keydown={forwardEventToMap} > <GeolocationControl {state} /> <!-- h-8 w-8 + p-0.5 sm:p-1 + 2px border => 9 sm: 10 in total--> </MapControlButton> {#if $compassLoaded} <div class="absolute top-0 left-0 m-0.5 h-0 w-0 sm:m-1"> <Compass_arrow class="compass_arrow" style={`rotate: calc(${-$compass}deg + 45deg); transform-origin: 50% 50%;`} /> </div> {/if} </div> </If> </div> </div> </div> <LoginToggle ignoreLoading={true} {state}> {#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback} <div class="pointer-events-none absolute top-0 left-0 flex h-full w-full items-center justify-center" > <Cross class="h-4 w-4" /> </div> {/if} <svelte:fragment slot="error" /> <!-- Add in an empty container to remove errors --> </LoginToggle> <If condition={state.previewedImage.map((i) => i !== undefined)}> <FloatOver extraClasses="p-1" on:close={() => state.previewedImage.setData(undefined)}> <div class="absolute right-4 top-4 h-8 w-8 cursor-pointer rounded-full bg-white/50 transition-colors duration-200 hover:bg-white" on:click={() => previewedImage.setData(undefined)} slot="close-button" > <XCircleIcon /> </div> <ImageOperations image={$previewedImage} /> </FloatOver> </If> {#if $selectedElement !== undefined && $selectedLayer !== undefined && !$selectedLayer.popupInFloatover} <!-- right modal with the selected element view --> <ModalRight on:close={() => { selectedElement.setData(undefined) }} > <div slot="close-button" /> <div class="normal-background absolute flex h-full w-full flex-col"> <SelectedElementTitle {state} layer={$selectedLayer} selectedElement={$selectedElement} /> <SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} /> </div> </ModalRight> {/if} {#if $selectedElement !== undefined && $selectedLayer !== undefined && $selectedLayer.popupInFloatover} <!-- Floatover with the selected element, if applicable --> <FloatOver on:close={() => { state.selectedElement.setData(undefined) }} > <div class="flex h-full w-full"> <SelectedElementView {state} layer={$selectedLayer} selectedElement={$selectedElement} /> </div> </FloatOver> {/if} <If condition={state.guistate.themeIsOpened}> <!-- Theme menu --> <FloatOver on:close={() => state.guistate.themeIsOpened.setData(false)}> <span slot="close-button"><!-- Disable the close button --></span> <TabbedGroup condition1={state.featureSwitches.featureSwitchFilter} tab={state.guistate.themeViewTabIndex} > <div slot="post-tablist"> <XCircleIcon class="mr-2 h-8 w-8" on:click={() => state.guistate.themeIsOpened.setData(false)} /> </div> <div class="flex" slot="title0"> <img class="block h-4 w-4" src={layout.icon} /> <Tr t={layout.title} /> </div> <div class="m-4 h-full" slot="content0"> <ThemeIntroPanel {state} /> </div> <div class="flex" slot="title1"> <If condition={state.featureSwitches.featureSwitchEnableExport}> <Download class="h-4 w-4" /> <Tr t={Translations.t.general.download.title} /> </If> </div> <div class="m-4" slot="content1"> <DownloadPanel {state} /> </div> <div slot="title2"> <Tr t={Translations.t.general.attribution.title} /> </div> <ToSvelte construct={() => new CopyrightPanel(state)} slot="content2" /> <div class="flex" slot="title3"> <Share class="h-4 w-4" /> <Tr t={Translations.t.general.sharescreen.title} /> </div> <div class="m-2" slot="content3"> <ShareScreen {state} /> </div> </TabbedGroup> </FloatOver> </If> <If condition={state.guistate.filtersPanelIsOpened}> <FloatOver on:close={() => state.guistate.filtersPanelIsOpened.setData(false)}> <FilterPanel {state} /> </FloatOver> </If> <IfHidden condition={state.guistate.backgroundLayerSelectionIsOpened}> <!-- background layer selector --> <FloatOver on:close={() => { state.guistate.backgroundLayerSelectionIsOpened.setData(false) }} > <div class="h-full p-2"> <RasterLayerOverview {availableLayers} map={state.map} mapproperties={state.mapProperties} userstate={state.userRelatedState} visible={state.guistate.backgroundLayerSelectionIsOpened} /> </div> </FloatOver> </IfHidden> <If condition={state.guistate.menuIsOpened}> <!-- Menu page --> <FloatOver on:close={() => state.guistate.menuIsOpened.setData(false)}> <span slot="close-button"><!-- Hide the default close button --></span> <TabbedGroup condition1={featureSwitches.featureSwitchEnableLogin} condition2={state.featureSwitches.featureSwitchCommunityIndex} tab={state.guistate.menuViewTabIndex} > <div slot="post-tablist"> <XCircleIcon class="mr-2 h-8 w-8" on:click={() => state.guistate.menuIsOpened.setData(false)} /> </div> <div class="flex" slot="title0"> <Tr t={Translations.t.general.menu.aboutMapComplete} /> </div> <div class="links-as-button links-w-full m-2 flex flex-col gap-y-1" slot="content0"> <Tr t={Translations.t.general.aboutMapComplete.intro} /> <a class="flex" href={Utils.HomepageLink()}> <Add class="h-6 w-6" /> <Tr t={Translations.t.general.backToIndex} /> </a> <a class="flex" href="https://github.com/pietervdvn/MapComplete/issues" target="_blank"> <Bug class="h-6 w-6" /> <Tr t={Translations.t.general.attribution.openIssueTracker} /> </a> <a class="flex" href="https://en.osm.town/@MapComplete" target="_blank"> <Mastodon class="h-6 w-6" /> <Tr t={Translations.t.general.attribution.followOnMastodon} /> </a> <a class="flex" href="https://liberapay.com/pietervdvn/" target="_blank"> <Liberapay class="h-6 w-6" /> <Tr t={Translations.t.general.attribution.donate} /> </a> <button class="small soft flex" on:click={() => state.guistate.communityIndexPanelIsOpened.setData(true)} > <Community class="h-6 w-6" /> <Tr t={Translations.t.communityIndex.title} /> </button> <If condition={featureSwitches.featureSwitchEnableLogin}> <OpenIdEditor mapProperties={state.mapProperties} /> <OpenJosm {state} /> <MapillaryLink large={false} mapProperties={state.mapProperties} /> </If> <button class="small soft flex" on:click={() => state.guistate.privacyPanelIsOpened.setData(true)} > <EyeIcon class="h-6 w-6 pr-1" /> <Tr t={Translations.t.privacy.title} /> </button> <div class="m-2 flex flex-col"> <ToSvelte construct={Hotkeys.generateDocumentationDynamic} /> </div> {Constants.vNumber} </div> <div class="flex" slot="title1"> <CogIcon class="h-6 w-6" /> <Tr t={UserRelatedState.usersettingsConfig.title.GetRenderValue({})} /> </div> <div class="links-as-button" slot="content1"> <!-- All shown components are set by 'usersettings.json', which happily uses some special visualisations created specifically for it --> <LoginToggle {state}> <div class="flex flex-col" slot="not-logged-in"> <LanguagePicker availableLanguages={layout.language} /> <Tr cls="alert" t={Translations.t.userinfo.notLoggedIn} /> <LoginButton clss="primary" osmConnection={state.osmConnection} /> </div> <SelectedElementView highlightedRendering={state.guistate.highlightedUserSetting} layer={UserRelatedState.usersettingsConfig} selectedElement={{ type: "Feature", properties: {}, geometry: { type: "Point", coordinates: [0, 0] }, }} {state} tags={state.userRelatedState.preferencesAsTags} /> </LoginToggle> </div> <div class="flex" slot="title2"> <HeartIcon class="h-6 w-6" /> <Tr t={Translations.t.favouritePoi.tab} /> </div> <div class="m-2 flex flex-col" slot="content2"> <h3> <Tr t={Translations.t.favouritePoi.title} /> </h3> <Favourites {state} /> </div> </TabbedGroup> </FloatOver> </If> <If condition={state.guistate.privacyPanelIsOpened}> <FloatOver on:close={() => state.guistate.privacyPanelIsOpened.setData(false)}> <div class="flex h-full flex-col overflow-hidden"> <h2 class="low-interaction m-0 flex items-center p-4"> <EyeIcon class="w-6 pr-2" /> <Tr t={Translations.t.privacy.title} /> </h2> <div class="overflow-auto p-4"> <PrivacyPolicy /> </div> </div> </FloatOver> </If> <If condition={state.guistate.communityIndexPanelIsOpened}> <FloatOver on:close={() => state.guistate.communityIndexPanelIsOpened.setData(false)}> <div class="flex h-full flex-col overflow-hidden"> <h2 class="low-interaction m-0 flex items-center p-4"> <Community class="h-6 w-6" /> <Tr t={Translations.t.communityIndex.title} /> </h2> <div class="overflow-auto p-4"> <CommunityIndexView location={state.mapProperties.location} /> </div> </div> </FloatOver> </If>