Compare commits
11 Commits
master
...
vue-contro
Author | SHA1 | Date | |
---|---|---|---|
|
9fe89c2d71 | ||
|
14674b774e | ||
|
4887acb917 | ||
|
547e64b317 | ||
|
318ccf6e33 | ||
|
7471bb794f | ||
|
c51d2ef554 | ||
|
4584410ae2 | ||
|
627e3219d1 | ||
|
c6502e5023 | ||
|
26d5d59b1b |
@ -1,3 +1,3 @@
|
|||||||
<component name="DependencyValidationManager">
|
<component name="DependencyValidationManager">
|
||||||
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&&!file:src/leaflet/control/CoordinatesControl.ts&&!file:src/leaflet/control/LinkControl.ts&&!file:src/leaflet/control/LogoControl.ts&&!file:src/leaflet/icon/PlayerIcon.ts&&!file:src/leaflet/icon/GenericIcon.ts&&!file:src/leaflet/tileLayer/DynmapTileLayer.ts&&!file:src/util/areas.ts&&!file:src/util/circles.ts&&!file:src/util/lines.ts&&!file:src/util/markers.ts&&!file[LiveAtlas]:standalone/*&&!file:src/model/LiveAtlasProjection.ts&&!file:src/leaflet/control/LiveAtlasLayerControl.ts&&!file[LiveAtlas]:patches/*&&!file[LiveAtlas]:public/*&&!file[LiveAtlas]:.idea/*&&!file[LiveAtlas]:.idea//*&&!file[LiveAtlas]:patches//*&&!file[LiveAtlas]:public//*&&!file[LiveAtlas]:standalone//*&&!file:FUNDING.yml&&!file:README.md&&!file:tsconfig.json&&!file:.gitignore&&!file:.env&&!file:LICENSE.md&&!file:package-lock.json&&!file:package.json&&!file:vite.config.ts&&!file:index.html&&!file:src/leaflet/control/LoadingControl.ts&&!file:src/scss/style.scss&&!file[LiveAtlas]:src/assets/icons//*&&!file:src/providers/OverviewerMapProvider.ts&&!file:src/providers/DynmapMapProvider.ts&&!file:src/leaflet/projection/OverviewerProjection.ts&&!file:src/leaflet/tileLayer/OverviewerTileLayer.ts&&!file:jest.config.ts&&!file:.npmignore&&!file:plugin.yml&&!file[LiveAtlas]:java/*&&!file[LiveAtlas]:java//*" />
|
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&&!file:src/leaflet/control/CoordinatesControl.ts&&!file:src/leaflet/control/LinkControl.ts&&!file:src/leaflet/control/LogoControl.ts&&!file:src/leaflet/icon/PlayerIcon.ts&&!file:src/leaflet/icon/GenericIcon.ts&&!file:src/leaflet/tileLayer/DynmapTileLayer.ts&&!file:src/util/areas.ts&&!file:src/util/circles.ts&&!file:src/util/lines.ts&&!file:src/util/markers.ts&&!file[LiveAtlas]:standalone/*&&!file:src/model/LiveAtlasProjection.ts&&!file:src/leaflet/control/LiveAtlasLayerControl.ts&&!file[LiveAtlas]:patches/*&&!file[LiveAtlas]:public/*&&!file[LiveAtlas]:.idea/*&&!file[LiveAtlas]:.idea//*&&!file[LiveAtlas]:patches//*&&!file[LiveAtlas]:public//*&&!file[LiveAtlas]:standalone//*&&!file:FUNDING.yml&&!file:README.md&&!file:tsconfig.json&&!file:.gitignore&&!file:.env&&!file:LICENSE.md&&!file:package-lock.json&&!file:package.json&&!file:vite.config.ts&&!file:index.html&&!file:src/leaflet/control/LoadingControl.ts&&!file:src/scss/style.scss&&!file[LiveAtlas]:src/assets/icons//*&&!file:src/providers/OverviewerMapProvider.ts&&!file:src/providers/DynmapMapProvider.ts&&!file:src/leaflet/projection/OverviewerProjection.ts&&!file:src/leaflet/tileLayer/OverviewerTileLayer.ts&&!file:jest.config.ts&&!file:.npmignore&&!file:plugin.yml&&!file[LiveAtlas]:java/*&&!file[LiveAtlas]:java//*&&!file:src/components/map/control/LoadingControl.vue" />
|
||||||
</component>
|
</component>
|
12
package-lock.json
generated
12
package-lock.json
generated
@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kyvg/vue3-notification": "2.3.0",
|
"@kyvg/vue3-notification": "2.3.0",
|
||||||
"@soerenmartius/vue3-clipboard": "^0.1",
|
"@soerenmartius/vue3-clipboard": "^0.1",
|
||||||
"leaflet": "git+https://github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
|
"leaflet": "git+https://github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
@ -7606,8 +7606,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/leaflet": {
|
"node_modules/leaflet": {
|
||||||
"version": "1.8.0-LiveAtlas",
|
"version": "1.8.0-LiveAtlas",
|
||||||
"resolved": "git+ssh://git@github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
|
"resolved": "git+ssh://git@github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
|
||||||
"integrity": "sha512-QJPpfKcc+xgcOzB6KlJEjdw4RJ41rMSeeTqPD7QsO1CvJ9uiJ/yhW4iSTKyhsmZ3JJDBw8Do2+cLevkddyLR9w==",
|
"integrity": "sha512-Fptn2BrpixOsx/+dAzObvWcdZML6+rCjSGxB8oeXt4sBCxHj4QuFB/8bAxMgzQW72oE0SytmMmLN+38WTRq3gA==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/leven": {
|
"node_modules/leven": {
|
||||||
@ -16196,9 +16196,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"leaflet": {
|
"leaflet": {
|
||||||
"version": "git+ssh://git@github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
|
"version": "git+ssh://git@github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
|
||||||
"integrity": "sha512-QJPpfKcc+xgcOzB6KlJEjdw4RJ41rMSeeTqPD7QsO1CvJ9uiJ/yhW4iSTKyhsmZ3JJDBw8Do2+cLevkddyLR9w==",
|
"integrity": "sha512-Fptn2BrpixOsx/+dAzObvWcdZML6+rCjSGxB8oeXt4sBCxHj4QuFB/8bAxMgzQW72oE0SytmMmLN+38WTRq3gA==",
|
||||||
"from": "leaflet@git+https://github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1"
|
"from": "leaflet@git+https://github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376"
|
||||||
},
|
},
|
||||||
"leven": {
|
"leven": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@kyvg/vue3-notification": "2.3.0",
|
"@kyvg/vue3-notification": "2.3.0",
|
||||||
"@soerenmartius/vue3-clipboard": "^0.1",
|
"@soerenmartius/vue3-clipboard": "^0.1",
|
||||||
"leaflet": "git+https://github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
|
"leaflet": "git+https://github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^1.1.0",
|
||||||
"vue": "^3.2.37",
|
"vue": "^3.2.37",
|
||||||
|
13
src/App.vue
13
src/App.vue
@ -15,8 +15,9 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Map></Map>
|
<Map v-slot="slotProps">
|
||||||
<ChatBox v-if="chatBoxEnabled" v-show="chatBoxEnabled && chatVisible"></ChatBox>
|
<MapUI v-if="slotProps.leaflet" :leaflet="slotProps.leaflet"></MapUI>
|
||||||
|
</Map>
|
||||||
<LoginModal v-if="loginEnabled" v-show="loginModalVisible" :required="loginRequired"></LoginModal>
|
<LoginModal v-if="loginEnabled" v-show="loginModalVisible" :required="loginRequired"></LoginModal>
|
||||||
<Sidebar></Sidebar>
|
<Sidebar></Sidebar>
|
||||||
<notifications position="bottom center" :speed="250" :max="3" :ignoreDuplicates="true" classes="notification" />
|
<notifications position="bottom center" :speed="250" :max="3" :ignoreDuplicates="true" classes="notification" />
|
||||||
@ -26,7 +27,6 @@
|
|||||||
import {computed, defineComponent, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
import {computed, defineComponent, onBeforeUnmount, onMounted, ref, watch} from 'vue';
|
||||||
import Map from './components/Map.vue';
|
import Map from './components/Map.vue';
|
||||||
import Sidebar from './components/Sidebar.vue';
|
import Sidebar from './components/Sidebar.vue';
|
||||||
import ChatBox from './components/ChatBox.vue';
|
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {ActionTypes} from "@/store/action-types";
|
import {ActionTypes} from "@/store/action-types";
|
||||||
import {parseUrl} from '@/util';
|
import {parseUrl} from '@/util';
|
||||||
@ -36,13 +36,14 @@ import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
|
|||||||
import LoginModal from "@/components/login/LoginModal.vue";
|
import LoginModal from "@/components/login/LoginModal.vue";
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
import {clearPlayerImageCache} from "@/util/images";
|
import {clearPlayerImageCache} from "@/util/images";
|
||||||
|
import MapUI from "@/components/MapUI.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
|
MapUI,
|
||||||
Map,
|
Map,
|
||||||
Sidebar,
|
Sidebar,
|
||||||
ChatBox,
|
|
||||||
LoginModal
|
LoginModal
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -54,8 +55,6 @@ export default defineComponent({
|
|||||||
currentUrl = computed(() => store.getters.url),
|
currentUrl = computed(() => store.getters.url),
|
||||||
currentServer = computed(() => store.state.currentServer),
|
currentServer = computed(() => store.state.currentServer),
|
||||||
configurationHash = computed(() => store.state.configurationHash),
|
configurationHash = computed(() => store.state.configurationHash),
|
||||||
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
|
||||||
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
|
|
||||||
playerImageUrl = computed(() => store.state.components.players.imageUrl),
|
playerImageUrl = computed(() => store.state.components.players.imageUrl),
|
||||||
|
|
||||||
loggedIn = computed(() => store.state.loggedIn), //Whether the user is currently logged in
|
loggedIn = computed(() => store.state.loggedIn), //Whether the user is currently logged in
|
||||||
@ -225,8 +224,6 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatBoxEnabled,
|
|
||||||
chatVisible,
|
|
||||||
loginEnabled,
|
loginEnabled,
|
||||||
loginRequired,
|
loginRequired,
|
||||||
loginModalVisible
|
loginModalVisible
|
||||||
|
@ -15,18 +15,18 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="chat">
|
<section class="chatbox">
|
||||||
<ul class="chat__messages" role="log" aria-live="polite" aria-relevant="additions">
|
<ul class="chatbox__messages" role="log" aria-live="polite" aria-relevant="additions">
|
||||||
<ChatMessage v-for="message in chatMessages" :key="message.timestamp" :message="message"></ChatMessage>
|
<ChatMessage v-for="message in chatMessages" :key="message.timestamp" :message="message"></ChatMessage>
|
||||||
<li v-if="!chatMessages.length" class="message message--skeleton" role="none">{{ messageNoMessages }}</li>
|
<li v-if="!chatMessages.length" class="message message--skeleton" role="none">{{ messageNoMessages }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form v-if="sendingEnabled" class="chat__form" @submit.prevent="sendMessage">
|
<form v-if="sendingEnabled" class="chatbox__form" @submit.prevent="sendMessage">
|
||||||
<div role="alert" v-if="sendingError" class="chat__error">{{ sendingError }}</div>
|
<div role="alert" v-if="sendingError" class="chatbox__error">{{ sendingError }}</div>
|
||||||
<input ref="chatInput" v-model="enteredMessage" class="chat__input" type="text" :maxlength="maxMessageLength"
|
<input ref="chatInput" v-model="enteredMessage" class="chatbox__input" type="text" :maxlength="maxMessageLength"
|
||||||
:placeholder="messagePlaceholder" :disabled="sendingMessage">
|
:placeholder="messagePlaceholder" :disabled="sendingMessage">
|
||||||
<button class="chat__send" :disabled="!enteredMessage || sendingMessage">{{ messageSend }}</button>
|
<button type="submit" class="chatbox__send" :disabled="!enteredMessage || sendingMessage">{{ messageSend }}</button>
|
||||||
</form>
|
</form>
|
||||||
<button type="button" v-if="loginRequired" class="chat__login" @click="login">{{ messageLogin }}</button>
|
<button type="button" v-if="loginRequired" class="chatbox__login" @click="login">{{ messageLogin }}</button>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -129,18 +129,12 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../scss/placeholders';
|
@import '../scss/placeholders';
|
||||||
|
|
||||||
.chat {
|
.chatbox {
|
||||||
@extend %panel;
|
@extend %panel;
|
||||||
position: absolute;
|
|
||||||
bottom: calc((var(--ui-element-spacing) * 2) + var(--ui-button-size));
|
|
||||||
left: calc((var(--ui-element-spacing) * 2) + var(--ui-button-size));
|
|
||||||
width: 50rem;
|
|
||||||
max-width: calc(100% - 8rem);
|
|
||||||
max-height: 20rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
.chat__messages {
|
.chatbox__messages {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@ -163,24 +157,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__form {
|
.chatbox__form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
margin: 1.5rem -1.5rem -1.5rem;
|
margin: 1.5rem -1.5rem -1.5rem;
|
||||||
|
|
||||||
.chat__input {
|
.chatbox__input {
|
||||||
border-bottom-left-radius: var(--border-radius);
|
border-bottom-left-radius: var(--border-radius);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__send {
|
.chatbox__send {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
border-radius: 0 0 var(--border-radius) 0;
|
border-radius: 0 0 var(--border-radius) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__error {
|
.chatbox__error {
|
||||||
background-color: var(--background-error);
|
background-color: var(--background-error);
|
||||||
color: var(--text-emphasis);
|
color: var(--text-emphasis);
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
@ -190,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat__login {
|
.chatbox__login {
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
padding: 1.2rem;
|
padding: 1.2rem;
|
||||||
background-color: var(--background-light);
|
background-color: var(--background-light);
|
||||||
@ -206,7 +200,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 320px) {
|
@media (max-width: 320px) {
|
||||||
.chat__messages .message + .message {
|
.chatbox__messages .message + .message {
|
||||||
margin-bottom: 0.7rem;
|
margin-bottom: 0.7rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,18 +22,10 @@
|
|||||||
<TileLayerOverlay v-for="[name, overlay] in overlays" :key="name" :options="overlay" :leaflet="leaflet"></TileLayerOverlay>
|
<TileLayerOverlay v-for="[name, overlay] in overlays" :key="name" :options="overlay" :leaflet="leaflet"></TileLayerOverlay>
|
||||||
<PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
|
<PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
|
||||||
<MarkerSetLayer v-for="[name, markerSet] in markerSets" :key="name" :markerSet="markerSet" :leaflet="leaflet"></MarkerSetLayer>
|
<MarkerSetLayer v-for="[name, markerSet] in markerSets" :key="name" :markerSet="markerSet" :leaflet="leaflet"></MarkerSetLayer>
|
||||||
|
|
||||||
<LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo" :leaflet="leaflet"></LogoControl>
|
|
||||||
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
|
|
||||||
<LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl>
|
|
||||||
<ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl>
|
|
||||||
|
|
||||||
<LoginControl v-if="loginEnabled" :leaflet="leaflet"></LoginControl>
|
|
||||||
<ChatControl v-if="chatBoxEnabled" :leaflet="leaflet"></ChatControl>
|
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MapContextMenu v-if="contextMenuEnabled && leaflet" :leaflet="leaflet"></MapContextMenu>
|
<slot :leaflet="leaflet"></slot>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@ -43,32 +35,18 @@ import {useStore} from '@/store';
|
|||||||
import TileLayer from "@/components/map/layer/TileLayer.vue";
|
import TileLayer from "@/components/map/layer/TileLayer.vue";
|
||||||
import PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
|
import PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
|
||||||
import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
|
import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
|
||||||
import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue";
|
|
||||||
import ClockControl from "@/components/map/control/ClockControl.vue";
|
|
||||||
import LinkControl from "@/components/map/control/LinkControl.vue";
|
|
||||||
import ChatControl from "@/components/map/control/ChatControl.vue";
|
|
||||||
import LogoControl from "@/components/map/control/LogoControl.vue";
|
|
||||||
import {MutationTypes} from "@/store/mutation-types";
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
import {LoadingControl} from "@/leaflet/control/LoadingControl";
|
|
||||||
import MapContextMenu from "@/components/map/MapContextMenu.vue";
|
|
||||||
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
|
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
|
||||||
import LoginControl from "@/components/map/control/LoginControl.vue";
|
|
||||||
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
|
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
|
||||||
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
TileLayerOverlay,
|
TileLayerOverlay,
|
||||||
MapContextMenu,
|
|
||||||
TileLayer,
|
TileLayer,
|
||||||
PlayersLayer,
|
PlayersLayer,
|
||||||
MarkerSetLayer,
|
MarkerSetLayer
|
||||||
CoordinatesControl,
|
|
||||||
ClockControl,
|
|
||||||
LinkControl,
|
|
||||||
ChatControl,
|
|
||||||
LogoControl,
|
|
||||||
LoginControl
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
@ -81,13 +59,6 @@ export default defineComponent({
|
|||||||
configuration = computed(() => store.state.configuration),
|
configuration = computed(() => store.state.configuration),
|
||||||
|
|
||||||
playerMarkersEnabled = computed(() => store.getters.playerMarkersEnabled),
|
playerMarkersEnabled = computed(() => store.getters.playerMarkersEnabled),
|
||||||
coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled),
|
|
||||||
clockControlEnabled = computed(() => store.getters.clockControlEnabled),
|
|
||||||
linkControlEnabled = computed(() => store.state.components.linkControl),
|
|
||||||
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
|
||||||
loginEnabled = computed(() => store.state.components.login),
|
|
||||||
contextMenuEnabled = computed(() => !store.state.ui.disableContextMenu),
|
|
||||||
logoControls = computed(() => store.state.components.logoControls),
|
|
||||||
|
|
||||||
currentWorld = computed(() => store.state.currentWorld),
|
currentWorld = computed(() => store.state.currentWorld),
|
||||||
currentMap = computed(() => store.state.currentMap),
|
currentMap = computed(() => store.state.currentMap),
|
||||||
@ -99,6 +70,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
//Location and zoom to pan to upon next projection change
|
//Location and zoom to pan to upon next projection change
|
||||||
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
|
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
|
||||||
|
pendingLayerUpdates = computed(() => !!store.state.pendingLayerUpdates.size),
|
||||||
|
|
||||||
mapTitle = computed(() => store.state.messages.mapTitle);
|
mapTitle = computed(() => store.state.messages.mapTitle);
|
||||||
|
|
||||||
@ -110,14 +82,7 @@ export default defineComponent({
|
|||||||
configuration,
|
configuration,
|
||||||
|
|
||||||
playerMarkersEnabled,
|
playerMarkersEnabled,
|
||||||
coordinatesControlEnabled,
|
|
||||||
clockControlEnabled,
|
|
||||||
linkControlEnabled,
|
|
||||||
chatBoxEnabled,
|
|
||||||
loginEnabled,
|
|
||||||
contextMenuEnabled,
|
|
||||||
|
|
||||||
logoControls,
|
|
||||||
followTarget,
|
followTarget,
|
||||||
viewTarget,
|
viewTarget,
|
||||||
parsedUrl,
|
parsedUrl,
|
||||||
@ -127,6 +92,7 @@ export default defineComponent({
|
|||||||
currentMap,
|
currentMap,
|
||||||
|
|
||||||
scheduledView,
|
scheduledView,
|
||||||
|
pendingLayerUpdates,
|
||||||
|
|
||||||
mapTitle
|
mapTitle
|
||||||
}
|
}
|
||||||
@ -227,6 +193,21 @@ export default defineComponent({
|
|||||||
this.scheduledView = viewTarget;
|
this.scheduledView = viewTarget;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async pendingLayerUpdates(size) {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
if(size) {
|
||||||
|
const updates = await store.dispatch(ActionTypes.POP_LAYER_UPDATES, undefined);
|
||||||
|
|
||||||
|
for (const update of updates) {
|
||||||
|
if(update[1]) {
|
||||||
|
this.leaflet.addLayer(update[0]);
|
||||||
|
} else {
|
||||||
|
this.leaflet.removeLayer(update[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
parsedUrl: {
|
parsedUrl: {
|
||||||
handler(newValue) {
|
handler(newValue) {
|
||||||
if(!newValue || !this.currentMap || !this.leaflet) {
|
if(!newValue || !this.currentMap || !this.leaflet) {
|
||||||
@ -253,9 +234,7 @@ export default defineComponent({
|
|||||||
center: new LatLng(0, 0),
|
center: new LatLng(0, 0),
|
||||||
fadeAnimation: false,
|
fadeAnimation: false,
|
||||||
zoomAnimation: true,
|
zoomAnimation: true,
|
||||||
zoomControl: true,
|
|
||||||
preferCanvas: true,
|
preferCanvas: true,
|
||||||
attributionControl: false,
|
|
||||||
crs: CRS.Simple,
|
crs: CRS.Simple,
|
||||||
worldCopyJump: false,
|
worldCopyJump: false,
|
||||||
// markerZoomAnimation: false,
|
// markerZoomAnimation: false,
|
||||||
@ -265,11 +244,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
this.leaflet.createPane('vectors');
|
this.leaflet.createPane('vectors');
|
||||||
|
|
||||||
this.leaflet.addControl(new LoadingControl({
|
|
||||||
position: 'topleft',
|
|
||||||
delayIndicator: 500,
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.leaflet.on('moveend', () => {
|
this.leaflet.on('moveend', () => {
|
||||||
if(this.currentMap) {
|
if(this.currentMap) {
|
||||||
useStore().commit(MutationTypes.SET_CURRENT_LOCATION, this.currentMap
|
useStore().commit(MutationTypes.SET_CURRENT_LOCATION, this.currentMap
|
||||||
|
218
src/components/MapUI.vue
Normal file
218
src/components/MapUI.vue
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2022 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div id="ui">
|
||||||
|
<div id="ui__top-center" class="ui__section">
|
||||||
|
<ClockControl v-if="clockControlEnabled"></ClockControl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ui__top-left" class="ui__section section--vertical">
|
||||||
|
<div class="ui__toolbar toolbar--vertical">
|
||||||
|
<LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo"></LogoControl>
|
||||||
|
<ZoomControl :leaflet="leaflet"></ZoomControl>
|
||||||
|
<LayerControl></LayerControl>
|
||||||
|
<LoadingControl :leaflet="leaflet" :delay="500"></LoadingControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ui__bottom-left" class="ui__section">
|
||||||
|
<div class="ui__toolbar toolbar--vertical">
|
||||||
|
<LoginControl v-if="loginEnabled"></LoginControl>
|
||||||
|
<ChatControl v-if="chatBoxEnabled"></ChatControl>
|
||||||
|
</div>
|
||||||
|
<div class="ui__toolbar">
|
||||||
|
<LinkControl v-if="linkControlEnabled"></LinkControl>
|
||||||
|
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="ui__top-right" class="ui__section">
|
||||||
|
</div>
|
||||||
|
<div id="ui__bottom-right" class="ui__section"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo" :leaflet="leaflet"></LogoControl>-->
|
||||||
|
<MapContextMenu v-if="contextMenuEnabled && leaflet" :leaflet="leaflet"></MapContextMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {computed, defineComponent} from "@vue/runtime-core";
|
||||||
|
import {useStore} from '@/store';
|
||||||
|
import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue";
|
||||||
|
import ClockControl from "@/components/map/control/ClockControl.vue";
|
||||||
|
import LinkControl from "@/components/map/control/LinkControl.vue";
|
||||||
|
import ChatControl from "@/components/map/control/ChatControl.vue";
|
||||||
|
import LogoControl from "@/components/map/control/LogoControl.vue";
|
||||||
|
import MapContextMenu from "@/components/map/MapContextMenu.vue";
|
||||||
|
import LoginControl from "@/components/map/control/LoginControl.vue";
|
||||||
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
|
import LoadingControl from "@/components/map/control/LoadingControl.vue";
|
||||||
|
import ZoomControl from "@/components/map/control/ZoomControl.vue";
|
||||||
|
import LayerControl from "@/components/map/control/LayerControl.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
leaflet: {
|
||||||
|
type: Object as () => LiveAtlasLeafletMap,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LayerControl,
|
||||||
|
ZoomControl,
|
||||||
|
LoadingControl,
|
||||||
|
LogoControl,
|
||||||
|
CoordinatesControl,
|
||||||
|
LinkControl,
|
||||||
|
ClockControl,
|
||||||
|
LoginControl,
|
||||||
|
ChatControl,
|
||||||
|
MapContextMenu
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore(),
|
||||||
|
contextMenuEnabled = computed(() => !store.state.ui.disableContextMenu),
|
||||||
|
coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled),
|
||||||
|
clockControlEnabled = computed(() => store.getters.clockControlEnabled),
|
||||||
|
linkControlEnabled = computed(() => store.state.components.linkControl),
|
||||||
|
chatBoxEnabled = computed(() => store.state.components.chatBox),
|
||||||
|
loginEnabled = computed(() => store.state.components.login),
|
||||||
|
|
||||||
|
logoControls = computed(() => store.state.components.logoControls);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contextMenuEnabled,
|
||||||
|
coordinatesControlEnabled,
|
||||||
|
clockControlEnabled,
|
||||||
|
linkControlEnabled,
|
||||||
|
chatBoxEnabled,
|
||||||
|
loginEnabled,
|
||||||
|
logoControls,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../scss/placeholders';
|
||||||
|
|
||||||
|
#ui {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100vh; /* Use explicit height to make safari happy */
|
||||||
|
width: 100vw;
|
||||||
|
pointer-events: none;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr min-content 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
z-index: 1003;
|
||||||
|
padding: var(--ui-element-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__section {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--ui-element-spacing);
|
||||||
|
align-items: start;
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__toolbar {
|
||||||
|
display: grid;
|
||||||
|
grid-gap: var(--ui-element-spacing);
|
||||||
|
grid-auto-flow: column;
|
||||||
|
align-items: start;
|
||||||
|
justify-items: start;
|
||||||
|
|
||||||
|
&.toolbar--vertical {
|
||||||
|
flex-direction: column;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__element {
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
display: none !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__button,
|
||||||
|
.ui__panel {
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__button {
|
||||||
|
@extend %button;
|
||||||
|
pointer-events: auto;
|
||||||
|
width: var(--ui-button-size);
|
||||||
|
height: var(--ui-button-size);
|
||||||
|
line-height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__panel {
|
||||||
|
@extend %panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: inherit;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
|
||||||
|
.ui__button {
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: calc(var(--border-radius) - 0.2rem);
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 0.1rem solid var(--border-color);
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui__top-center {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui__top-left {
|
||||||
|
grid-row: 1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui__bottom-left {
|
||||||
|
grid-row: -1;
|
||||||
|
grid-column: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui__bottom-right {
|
||||||
|
grid-row: -1;
|
||||||
|
grid-column: -1;
|
||||||
|
}
|
||||||
|
</style>
|
@ -14,26 +14,48 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="chat">
|
||||||
|
<button class="ui__element ui__button" type="button" :title="buttonTitle" :aria-expanded="chatVisible"
|
||||||
|
@click.prevent.stop="handleClick"
|
||||||
|
@keydown.right.prevent.stop="handleKeydown">
|
||||||
|
<SvgIcon name="chat"></SvgIcon>
|
||||||
|
</button>
|
||||||
|
<ChatBox v-show="chatVisible"></ChatBox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {computed, defineComponent} from "@vue/runtime-core";
|
||||||
import {ChatControl} from "@/leaflet/control/ChatControl";
|
import {useStore} from "@/store";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
|
||||||
|
import "@/assets/icons/chat.svg";
|
||||||
|
import ChatBox from "@/components/ChatBox.vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
components: {
|
||||||
leaflet: {
|
ChatBox,
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
SvgIcon,
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
setup() {
|
||||||
const control = new ChatControl({
|
const store = useStore(),
|
||||||
position: 'topleft',
|
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
|
||||||
});
|
buttonTitle = computed(() => store.state.messages.chatTitle);
|
||||||
|
|
||||||
onMounted(() => props.leaflet.addControl(control));
|
const handleClick = () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'chat'),
|
||||||
onUnmounted(() => props.leaflet.removeControl(control));
|
handleKeydown = () =>
|
||||||
|
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'chat', state: true});
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttonTitle,
|
||||||
|
chatVisible,
|
||||||
|
|
||||||
|
handleClick,
|
||||||
|
handleKeydown
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -41,3 +63,19 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chat {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.chatbox {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 50rem;
|
||||||
|
max-width: calc(100vw - 8rem);
|
||||||
|
max-height: 20rem;
|
||||||
|
left: calc(100% + var(--ui-element-spacing));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -14,43 +14,205 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="{'clock': true, 'ui__element': true, 'ui__panel': digital}">
|
||||||
|
<div class="clock__sun" :style="sunStyle">
|
||||||
|
<SvgIcon :name="sunIcon"></SvgIcon>
|
||||||
|
</div>
|
||||||
|
<div class="clock__moon" :style="moonStyle">
|
||||||
|
<SvgIcon :name="moonIcon"></SvgIcon>
|
||||||
|
</div>
|
||||||
|
<div v-if="showTime" :class="{'clock__time': true, 'day': minecraftTime?.day, 'night': minecraftTime?.night}">
|
||||||
|
{{ formattedTime }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {computed, defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {computed, defineComponent} from "@vue/runtime-core";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {ClockControl, ClockControlOptions} from "@/leaflet/control/ClockControl";
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import {getMinecraftTime} from "@/util";
|
||||||
import {watch} from "vue";
|
|
||||||
|
import "@/assets/icons/clock_moon.svg";
|
||||||
|
import "@/assets/icons/clock_moon_rain.svg";
|
||||||
|
import "@/assets/icons/clock_moon_storm.svg";
|
||||||
|
import "@/assets/icons/clock_sun.svg";
|
||||||
|
import "@/assets/icons/clock_sun_rain.svg";
|
||||||
|
import "@/assets/icons/clock_sun_storm.svg";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
components: {SvgIcon},
|
||||||
leaflet: {
|
setup() {
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
|
||||||
required: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const store = useStore(),
|
const store = useStore(),
|
||||||
componentSettings = computed(() => store.state.components.clockControl);
|
componentSettings = computed(() => store.state.components.clockControl),
|
||||||
let control = new ClockControl(componentSettings.value as ClockControlOptions) as ClockControl;
|
digital = computed(() => componentSettings.value!.showTimeOfDay && !componentSettings.value!.showWeather && componentSettings.value!.showDigitalClock),
|
||||||
|
showTime = computed(() => componentSettings.value!.showDigitalClock),
|
||||||
|
|
||||||
watch(componentSettings, (newSettings) => {
|
worldState = computed(() => store.state.currentWorldState),
|
||||||
props.leaflet.removeControl(control);
|
minecraftTime = computed(() => getMinecraftTime(worldState.value.timeOfDay)),
|
||||||
|
|
||||||
if(!newSettings) {
|
formattedTime = computed(() => {
|
||||||
return;
|
if (minecraftTime.value) {
|
||||||
|
return [
|
||||||
|
minecraftTime.value.hours.toString().padStart(2, '0'),
|
||||||
|
minecraftTime.value.minutes.toString().padStart(2, '0')
|
||||||
|
].join(':');
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
sunAngle = computed(() => {
|
||||||
|
const timeOfDay = worldState.value.timeOfDay;
|
||||||
|
|
||||||
|
if (timeOfDay > 23100 || timeOfDay < 12900) {
|
||||||
|
//day mode
|
||||||
|
let movedTime = timeOfDay + 900;
|
||||||
|
movedTime = (movedTime >= 24000) ? movedTime - 24000 : movedTime;
|
||||||
|
//Now we have 0 -> 13800 for the day period
|
||||||
|
//Divide by 13800*2=27600 instead of 24000 to compress day
|
||||||
|
return ((movedTime) / 27600 * 2 * Math.PI);
|
||||||
|
} else {
|
||||||
|
//night mode
|
||||||
|
const movedTime = timeOfDay - 12900;
|
||||||
|
//Now we have 0 -> 10200 for the night period
|
||||||
|
//Divide by 10200*2=20400 instead of 24000 to expand night
|
||||||
|
return Math.PI + ((movedTime) / 20400 * 2 * Math.PI);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
moonAngle = computed(() => sunAngle.value + Math.PI),
|
||||||
|
|
||||||
|
sunStyle = computed(() => {
|
||||||
|
if (worldState.value.timeOfDay >= 0) {
|
||||||
|
return {'transform': 'translate(' + Math.round(-50 * Math.cos(sunAngle.value)) + 'px, ' + Math.round(-50 * Math.sin(sunAngle.value)) + 'px)'};
|
||||||
|
} else {
|
||||||
|
return {'transform': 'translate(-150px, -150px)'};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
moonStyle = computed(() => {
|
||||||
|
if (worldState.value.timeOfDay >= 0) {
|
||||||
|
return {'transform': 'translate(' + Math.round(-50 * Math.cos(moonAngle.value)) + 'px, ' + Math.round(-50 * Math.sin(moonAngle.value)) + 'px)'};
|
||||||
|
} else {
|
||||||
|
return {'transform': 'translate(-150px, -150px)'};
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
sunIcon = computed(() => {
|
||||||
|
if (componentSettings.value!.showWeather) {
|
||||||
|
if (worldState.value.thundering) {
|
||||||
|
return 'clock_sun_storm';
|
||||||
|
} else if (worldState.value.raining) {
|
||||||
|
return 'clock_sun_rain';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
control = new ClockControl(newSettings as ClockControlOptions);
|
return 'clock_sun';
|
||||||
props.leaflet.addControl(control);
|
}),
|
||||||
}, {deep: true});
|
|
||||||
|
|
||||||
onMounted(() => props.leaflet.addControl(control));
|
moonIcon = computed(() => {
|
||||||
onUnmounted(() => props.leaflet.removeControl(control));
|
if (componentSettings.value!.showWeather) {
|
||||||
},
|
if (worldState.value.thundering) {
|
||||||
|
return 'clock_moon_storm';
|
||||||
|
} else if (worldState.value.raining) {
|
||||||
|
return 'clock_moon_rain';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
return 'clock_moon';
|
||||||
return null;
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
digital,
|
||||||
|
showTime,
|
||||||
|
|
||||||
|
minecraftTime,
|
||||||
|
formattedTime,
|
||||||
|
|
||||||
|
sunStyle,
|
||||||
|
moonStyle,
|
||||||
|
|
||||||
|
sunIcon,
|
||||||
|
moonIcon
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../scss/placeholders';
|
||||||
|
|
||||||
|
.clock {
|
||||||
|
@extend %panel;
|
||||||
|
position: relative;
|
||||||
|
width: 15rem;
|
||||||
|
height: 6rem;
|
||||||
|
z-index: 50;
|
||||||
|
font-family: monospace;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.clock__time {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
margin-top: auto;
|
||||||
|
background-color: var(--background-base);
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0.1rem 0.1rem 0;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
|
||||||
|
&.night {
|
||||||
|
color: var(--text-night);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.day {
|
||||||
|
color: var(--text-day);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.night, &.day {
|
||||||
|
transition: color 8s 8s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock__sun,
|
||||||
|
.clock__moon {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 15rem;
|
||||||
|
height: 12rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.clock--digital {
|
||||||
|
justify-content: center;
|
||||||
|
height: var(--ui-button-size);
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
.clock__sun,
|
||||||
|
.clock__moon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock__time {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px), (max-height: 480px) {
|
||||||
|
transform: scale(calc((1/6)*5));
|
||||||
|
transform-origin: top center
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -14,12 +14,24 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ui__element ui__panel location">
|
||||||
|
<span class="value coordinates" :data-label="componentSettings.label">{{ formattedCoordinates }}</span>
|
||||||
|
<span v-if="componentSettings.showChunk" class="value chunk"
|
||||||
|
:data-label="chunkLabel">{{ formattedChunk }}</span>
|
||||||
|
<span v-if="componentSettings.showRegion" class="value region"
|
||||||
|
:data-label="regionLabel">{{ formattedRegion }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {computed, defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {CoordinatesControl, CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
|
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
import {watch} from "vue";
|
import {onMounted, ref} from "vue";
|
||||||
|
import {Coordinate, CoordinatesControlOptions} from "@/index";
|
||||||
|
import {LeafletMouseEvent} from "leaflet";
|
||||||
|
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@ -31,26 +43,129 @@ export default defineComponent({
|
|||||||
|
|
||||||
setup(props) {
|
setup(props) {
|
||||||
const store = useStore(),
|
const store = useStore(),
|
||||||
componentSettings = computed(() => store.state.components.coordinatesControl);
|
componentSettings = computed(() => store.state.components.coordinatesControl as CoordinatesControlOptions),
|
||||||
let control = new CoordinatesControl(componentSettings.value as CoordinatesControlOptions);
|
currentMap = computed(() => store.state.currentMap),
|
||||||
|
|
||||||
watch(componentSettings, (newSettings) => {
|
chunkLabel = computed(() => store.state.messages.locationChunk),
|
||||||
props.leaflet.removeControl(control);
|
regionLabel = computed(() => store.state.messages.locationRegion),
|
||||||
|
|
||||||
if(!newSettings) {
|
coordinates = ref<Coordinate|null>(null),
|
||||||
|
|
||||||
|
formattedCoordinates = computed(() => {
|
||||||
|
if(coordinates.value) {
|
||||||
|
const x = Math.round(coordinates.value.x).toString().padStart(5, ' '),
|
||||||
|
y = coordinates.value.y.toString().padStart(3, ' '),
|
||||||
|
z = Math.round(coordinates.value.z).toString().padStart(5, ' ');
|
||||||
|
|
||||||
|
return componentSettings.value!.showY ? `${x}, ${y}, ${z}` : `${x}, ${z}`;
|
||||||
|
} else {
|
||||||
|
return componentSettings.value!.showY ? '-----, ---, -----' : '-----, -----';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
formattedChunk = computed(() => {
|
||||||
|
if(coordinates.value) {
|
||||||
|
const chunkX = Math.floor(coordinates.value.x / 16).toString().padStart(4, ' '),
|
||||||
|
chunkZ = Math.floor(coordinates.value.z / 16).toString().padStart(4, ' ');
|
||||||
|
|
||||||
|
return `${chunkX}, ${chunkZ}`;
|
||||||
|
} else {
|
||||||
|
return '----, ----'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
formattedRegion = computed(() => {
|
||||||
|
if(coordinates.value) {
|
||||||
|
const regionX = Math.floor(coordinates.value.x / 512).toString().padStart(3, ' '),
|
||||||
|
regionZ = Math.floor(coordinates.value.z / 512).toString().padStart(3, ' ');
|
||||||
|
|
||||||
|
return `r.${regionX}, ${regionZ}.mca`;
|
||||||
|
} else {
|
||||||
|
return '--------------';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onMouseMove = (event: LeafletMouseEvent) => {
|
||||||
|
if (!store.state.currentMap) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
control = new CoordinatesControl(newSettings as CoordinatesControlOptions);
|
coordinates.value = store.state.currentMap.latLngToLocation(event.latlng, store.state.currentWorld!.seaLevel + 1);
|
||||||
props.leaflet.addControl(control);
|
}
|
||||||
}, {deep: true});
|
|
||||||
|
|
||||||
onMounted(() => props.leaflet.addControl(control));
|
const onMouseOut = () => coordinates.value = null;
|
||||||
onUnmounted(() => props.leaflet.removeControl(control));
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
watch(currentMap, newValue => {
|
||||||
return null;
|
if(!newValue) {
|
||||||
|
coordinates.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
props.leaflet.on('mousemove', onMouseMove);
|
||||||
|
props.leaflet.on('mouseout', onMouseOut);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
props.leaflet.off('mousemove', onMouseMove);
|
||||||
|
props.leaflet.off('mouseout', onMouseOut);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
componentSettings,
|
||||||
|
chunkLabel,
|
||||||
|
regionLabel,
|
||||||
|
formattedCoordinates,
|
||||||
|
formattedChunk,
|
||||||
|
formattedRegion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.location {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.value {
|
||||||
|
line-height: 1;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
font-size: 2rem;
|
||||||
|
|
||||||
|
&[data-label]:before {
|
||||||
|
content: attr(data-label);
|
||||||
|
display: block;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-family: Raleway, sans-serif;;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .value {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.region {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px), (max-height: 480px) {
|
||||||
|
.value {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 384px) {
|
||||||
|
.chunk {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
179
src/components/map/control/LayerControl.vue
Normal file
179
src/components/map/control/LayerControl.vue
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2022 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="layers">
|
||||||
|
<button ref="button" type="button" class="ui__element ui__button" title="Layers" :aria-expanded="listVisible"
|
||||||
|
@click.prevent="toggleList"
|
||||||
|
@keydown.right.prevent.stop="toggleList">
|
||||||
|
<SvgIcon name="layers"></SvgIcon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<section ref="list" :hidden="!listVisible" class="ui__element ui__panel layers__list"
|
||||||
|
:style="listStyle" @keydown="handleListKeydown">
|
||||||
|
<div class="layers__base"></div>
|
||||||
|
<div class="layers__overlays">
|
||||||
|
<label v-for="layer in overlayLayers" :key="stamp(layer.layer)" class="layer checkbox">
|
||||||
|
<input type="checkbox" :checked="layer.enabled" @keydown.space.prevent="toggleLayer(layer.layer)"
|
||||||
|
@input.prevent="toggleLayer(layer.layer)">
|
||||||
|
<SvgIcon name="checkbox"></SvgIcon>
|
||||||
|
<span>{{ layer.name }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
|
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
|
||||||
|
import '@/assets/icons/layers.svg';
|
||||||
|
import '@/assets/icons/checkbox.svg';
|
||||||
|
import {stamp} from "leaflet";
|
||||||
|
import {toggleLayer} from "@/util/layers";
|
||||||
|
import {nextTick, onMounted, ref} from "vue";
|
||||||
|
import {handleKeyboardEvent} from "@/util/events";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {SvgIcon},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const store = useStore(),
|
||||||
|
overlayLayers = computed(() => store.state.sortedLayers.filter(layer => layer.overlay)),
|
||||||
|
baseLayers = computed(() => store.state.sortedLayers.filter(layer => !layer.overlay)),
|
||||||
|
listVisible = computed(() => store.state.ui.visibleElements.has('layers')),
|
||||||
|
listStyle = ref({'max-height': 'auto'}),
|
||||||
|
|
||||||
|
button = ref<HTMLButtonElement|null>(null),
|
||||||
|
list = ref<HTMLElement|null>(null);
|
||||||
|
|
||||||
|
const toggleList = () => listVisible.value ? closeList() : openList();
|
||||||
|
|
||||||
|
const openList = () =>
|
||||||
|
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: true});
|
||||||
|
|
||||||
|
const closeList = () =>
|
||||||
|
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: false});
|
||||||
|
|
||||||
|
const handleListKeydown = (event: KeyboardEvent) => {
|
||||||
|
if(event.key === 'ArrowLeft') {
|
||||||
|
closeList();
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements = Array.from((list.value as HTMLElement).querySelectorAll('input')) as HTMLElement[];
|
||||||
|
handleKeyboardEvent(event as KeyboardEvent, elements);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
const y = button.value!.getBoundingClientRect().y;
|
||||||
|
|
||||||
|
//Limit height to remaining vertical space
|
||||||
|
//Avoid covering bottom bar
|
||||||
|
listStyle.value['max-height'] = `calc(100vh - ${(y + 10 + 60)}px)`;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(listVisible, visible => {
|
||||||
|
if(visible) {
|
||||||
|
const firstCheckbox = (list.value as HTMLElement).querySelector('.checkbox');
|
||||||
|
|
||||||
|
if(firstCheckbox instanceof HTMLElement) {
|
||||||
|
nextTick(() => firstCheckbox.focus());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nextTick(() => (button.value as HTMLButtonElement).focus());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
handleResize();
|
||||||
|
});
|
||||||
|
onUnmounted(() => window.addEventListener('resize', handleResize));
|
||||||
|
|
||||||
|
return {
|
||||||
|
overlayLayers,
|
||||||
|
baseLayers,
|
||||||
|
listVisible,
|
||||||
|
listStyle,
|
||||||
|
|
||||||
|
button,
|
||||||
|
list,
|
||||||
|
|
||||||
|
toggleList,
|
||||||
|
openList,
|
||||||
|
closeList,
|
||||||
|
handleListKeydown,
|
||||||
|
toggleLayer,
|
||||||
|
stamp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../scss/placeholders';
|
||||||
|
|
||||||
|
.layers {
|
||||||
|
width: auto;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-base);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.layers__list[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers__list {
|
||||||
|
@extend %panel;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: calc(var(--ui-element-spacing) + var(--ui-button-size));
|
||||||
|
overflow: auto;
|
||||||
|
max-width: calc(100vw - 14rem);
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
@media screen and (max-width: 400px) {
|
||||||
|
max-width: calc(100vw - 13rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layers__overlays {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.8rem 0 0.7rem;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: -0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: -0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -13,31 +13,34 @@
|
|||||||
- See the License for the specific language governing permissions and
|
- See the License for the specific language governing permissions and
|
||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
<template>
|
||||||
|
<button class="ui__element ui__button" type="button" :title="linkTitle"
|
||||||
|
v-clipboard:copy="url" v-clipboard:success="copySuccess" v-clipboard:error="copyError">
|
||||||
|
<SvgIcon name="link"></SvgIcon>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {computed, defineComponent} from "@vue/runtime-core";
|
||||||
import {LinkControl} from "@/leaflet/control/LinkControl";
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import {useStore} from "@/store";
|
||||||
|
import {clipboardError, clipboardSuccess} from "@/util";
|
||||||
|
import '@/assets/icons/link.svg';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
components: {SvgIcon},
|
||||||
leaflet: {
|
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
setup() {
|
||||||
required: true,
|
const store = useStore(),
|
||||||
|
linkTitle = computed(() => store.state.messages.linkTitle),
|
||||||
|
url = computed(() => window.location.href.split("#")[0] + store.getters.url);
|
||||||
|
|
||||||
|
return {
|
||||||
|
copySuccess: clipboardSuccess(store),
|
||||||
|
copyError: clipboardError(store),
|
||||||
|
linkTitle,
|
||||||
|
url
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const control = new LinkControl({
|
|
||||||
position: 'bottomleft',
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => props.leaflet.addControl(control));
|
|
||||||
onUnmounted(() => props.leaflet.removeControl(control));
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
226
src/components/map/control/LoadingControl.vue
Normal file
226
src/components/map/control/LoadingControl.vue
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2022 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-
|
||||||
|
- Portions of this file are taken from Leaflet.loading:
|
||||||
|
-
|
||||||
|
- Copyright (c) 2013 Eric Brelsford
|
||||||
|
-
|
||||||
|
- Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
- of this software and associated documentation files (the "Software"), to deal
|
||||||
|
- in the Software without restriction, including without limitation the rights
|
||||||
|
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
- copies of the Software, and to permit persons to whom the Software is
|
||||||
|
- furnished to do so, subject to the following conditions:
|
||||||
|
-
|
||||||
|
- The above copyright notice and this permission notice shall be included in
|
||||||
|
- all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
- THE SOFTWARE.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ui__element ui__button loading" :title="loadingTitle" :hidden="!showIndicator">
|
||||||
|
<SvgIcon name="loading"></SvgIcon>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
|
||||||
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {Layer, LayerEvent, LeafletEvent, TileLayer} from "leaflet";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import '@/assets/icons/loading.svg';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {SvgIcon},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
leaflet: {
|
||||||
|
type: Object as () => LiveAtlasLeafletMap,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore(),
|
||||||
|
loadingTitle = computed(() => store.state.messages.loadingTitle),
|
||||||
|
dataLoaders = ref<Set<number>>(new Set()),
|
||||||
|
showIndicator = ref<boolean>(false);
|
||||||
|
|
||||||
|
let delayIndicatorTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
const addLayerListeners = () => {
|
||||||
|
// Add listeners for begin and end of load to any layers already
|
||||||
|
// on the map
|
||||||
|
props.leaflet.eachLayer((layer: Layer) => {
|
||||||
|
if(!(layer instanceof TileLayer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(layer.isLoading()) {
|
||||||
|
dataLoaders.value.add((layer as any)._leaflet_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.on('loading', handleLoading);
|
||||||
|
layer.on('load', handleLoad);
|
||||||
|
});
|
||||||
|
|
||||||
|
// When a layer is added to the map, add listeners for begin and
|
||||||
|
// end of load
|
||||||
|
props.leaflet.on('layeradd', layerAdd);
|
||||||
|
props.leaflet.on('layerremove', layerRemove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeLayerListeners = () => {
|
||||||
|
// Remove listeners for begin and end of load from all layers
|
||||||
|
props.leaflet.eachLayer((layer: Layer) => {
|
||||||
|
if(!(layer instanceof TileLayer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dataLoaders.value.delete((layer as any)._leaflet_id);
|
||||||
|
|
||||||
|
layer.off('loading', handleLoading);
|
||||||
|
layer.off('load', handleLoad);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove layeradd/layerremove listener from map
|
||||||
|
props.leaflet.off('layeradd', layerAdd);
|
||||||
|
props.leaflet.off('layerremove', layerRemove);
|
||||||
|
};
|
||||||
|
|
||||||
|
const layerAdd = (e: LayerEvent) => {
|
||||||
|
if(!(e.layer instanceof TileLayer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(e.layer.isLoading()) {
|
||||||
|
handleLoading(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.layer.on('loading', handleLoading);
|
||||||
|
e.layer.on('load', handleLoad);
|
||||||
|
} catch (exception) {
|
||||||
|
console.warn('L.Control.Loading: Tried and failed to add ' +
|
||||||
|
' event handlers to layer', e.layer);
|
||||||
|
console.warn('L.Control.Loading: Full details', exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const layerRemove = (e: LayerEvent) => {
|
||||||
|
if(!(e.layer instanceof TileLayer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoad(e);
|
||||||
|
|
||||||
|
try {
|
||||||
|
e.layer.off('loading', handleLoading);
|
||||||
|
e.layer.off('load', handleLoad);
|
||||||
|
} catch (exception) {
|
||||||
|
console.warn('L.Control.Loading: Tried and failed to remove ' +
|
||||||
|
'event handlers from layer', e.layer);
|
||||||
|
console.warn('L.Control.Loading: Full details', exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLoading = (e: LeafletEvent) => dataLoaders.value.add(getEventId(e))
|
||||||
|
const handleLoad = (e: LeafletEvent) => dataLoaders.value.delete(getEventId(e));
|
||||||
|
|
||||||
|
const getEventId = (e: any) => {
|
||||||
|
if (e.id) {
|
||||||
|
return e.id;
|
||||||
|
} else if (e.layer) {
|
||||||
|
return e.layer._leaflet_id;
|
||||||
|
}
|
||||||
|
return e.target._leaflet_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(dataLoaders, (newValue) => {
|
||||||
|
if(props.delay) { // If we are delaying showing the indicator
|
||||||
|
if(newValue.size > 0) {
|
||||||
|
// If we're not already waiting for that delay, set up a timeout.
|
||||||
|
if(!delayIndicatorTimeout) {
|
||||||
|
setTimeout(() => showIndicator.value = true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If removing this loader means we're in no danger of loading,
|
||||||
|
// clear the timeout. This prevents old delays from instantly
|
||||||
|
// triggering the indicator.
|
||||||
|
showIndicator.value = false;
|
||||||
|
clearTimeout(Number(delayIndicatorTimeout));
|
||||||
|
delayIndicatorTimeout = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// Otherwise update the indicator immediately
|
||||||
|
showIndicator.value = !!newValue.size;
|
||||||
|
}
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Add listeners to the map for (custom) dataloading and dataload
|
||||||
|
// events, eg, for AJAX calls that affect the map but will not be
|
||||||
|
// reflected in the above layer events.
|
||||||
|
props.leaflet.on('dataloading', handleLoading);
|
||||||
|
props.leaflet.on('dataload', handleLoad);
|
||||||
|
|
||||||
|
addLayerListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
props.leaflet.off('dataloading', handleLoading);
|
||||||
|
props.leaflet.off('dataload', handleLoad);
|
||||||
|
|
||||||
|
removeLayerListeners();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadingTitle,
|
||||||
|
dataLoaders,
|
||||||
|
showIndicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.loading {
|
||||||
|
cursor: wait;
|
||||||
|
animation: fade 0.3s linear;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
|
||||||
|
&:hover, &:active, &:focus {
|
||||||
|
background-color: var(--background-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -14,30 +14,56 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button class="ui__element ui__button" type="button" :title="buttonTitle" :aria-expanded="modalVisible"
|
||||||
|
@click.prevent.stop="handleClick"
|
||||||
|
@keydown.right.prevent.stop="handleClick">
|
||||||
|
<SvgIcon :name="loggedIn ? 'logout' : 'login'"></SvgIcon>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {computed, defineComponent} from "@vue/runtime-core";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import SvgIcon from "@/components/SvgIcon.vue";
|
||||||
import {LoginControl} from "@/leaflet/control/LoginControl";
|
import {useStore} from "@/store";
|
||||||
|
import {ActionTypes} from "@/store/action-types";
|
||||||
|
import {notify} from "@kyvg/vue3-notification";
|
||||||
|
|
||||||
|
import "@/assets/icons/login.svg";
|
||||||
|
import "@/assets/icons/logout.svg";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
components: {SvgIcon},
|
||||||
leaflet: {
|
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
setup() {
|
||||||
required: true,
|
const store = useStore(),
|
||||||
|
loggedIn = computed(() => store.state.loggedIn),
|
||||||
|
buttonTitle = computed(() => loggedIn.value ? store.state.messages.logoutTitle : store.state.messages.loginTitle),
|
||||||
|
modalVisible = computed(() => store.state.ui.visibleModal === 'login');
|
||||||
|
|
||||||
|
const handleClick = async () => {
|
||||||
|
const logoutSuccess = computed(() => store.state.messages.logoutSuccess),
|
||||||
|
logoutError = computed(() => store.state.messages.logoutErrorUnknown);
|
||||||
|
|
||||||
|
if (loggedIn.value) {
|
||||||
|
try {
|
||||||
|
await store.dispatch(ActionTypes.LOGOUT, undefined);
|
||||||
|
notify(logoutSuccess.value);
|
||||||
|
} catch(e) {
|
||||||
|
notify(logoutError.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await store.dispatch(ActionTypes.LOGIN, null)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
modalVisible,
|
||||||
|
loggedIn,
|
||||||
|
buttonTitle,
|
||||||
|
|
||||||
|
handleClick
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const control = new LoginControl({
|
|
||||||
position: 'topleft',
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => props.leaflet.addControl(control));
|
|
||||||
onUnmounted(() => props.leaflet.removeControl(control));
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,45 +14,47 @@
|
|||||||
- limitations under the License.
|
- limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="ui__element ui__button logo">
|
||||||
|
<a v-if="options.url" :href="options.url" :aria-label="options.text">
|
||||||
|
<img v-if="options.image" :src="options.image" :alt="options.text" />
|
||||||
|
<template v-else>{{ options.text }}</template>
|
||||||
|
</a>
|
||||||
|
<template v-else>
|
||||||
|
<img v-if="options.image" :src="options.image" :alt="options.text" />
|
||||||
|
<template v-else>{{ options.text }}</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent} from "@vue/runtime-core";
|
import {defineComponent} from "@vue/runtime-core";
|
||||||
import {LogoControl, LogoControlOptions} from "@/leaflet/control/LogoControl";
|
import {LogoControlOptions} from "@/index";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object as () => LogoControlOptions,
|
type: Object as () => LogoControlOptions,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
|
||||||
leaflet: {
|
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
|
||||||
required: true,
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
setup(props) {
|
|
||||||
const control = new LogoControl(props.options);
|
|
||||||
|
|
||||||
return {
|
|
||||||
control,
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
|
|
||||||
mounted() {
|
|
||||||
this.leaflet.addControl(this.control);
|
|
||||||
},
|
|
||||||
|
|
||||||
unmounted() {
|
|
||||||
this.leaflet.removeControl(this.control);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
|
.logo {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: auto;
|
||||||
|
min-width: var(--ui-button-size);
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
a {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0.8rem 0.8rem 0.7rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
88
src/components/map/control/ZoomControl.vue
Normal file
88
src/components/map/control/ZoomControl.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
- Copyright 2022 James Lyne
|
||||||
|
-
|
||||||
|
- Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
- you may not use this file except in compliance with the License.
|
||||||
|
- You may obtain a copy of the License at
|
||||||
|
-
|
||||||
|
- http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
-
|
||||||
|
- Unless required by applicable law or agreed to in writing, software
|
||||||
|
- distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
- See the License for the specific language governing permissions and
|
||||||
|
- limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="zoom ui__element ui__group">
|
||||||
|
<button class="ui__button" type="button" title="Zoom in" aria-label="Zoom in"
|
||||||
|
:disabled="!canZoomIn" @click.prevent="zoomIn">
|
||||||
|
<span aria-hidden="true">+</span>
|
||||||
|
</button>
|
||||||
|
<button class="ui__button" type="button" title="Zoom out" aria-label="Zoom out"
|
||||||
|
:disabled="!canZoomOut" @click.prevent="zoomOut">
|
||||||
|
<span aria-hidden="true">−</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {defineComponent, onUnmounted} from "@vue/runtime-core";
|
||||||
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
leaflet: {
|
||||||
|
type: Object as () => LiveAtlasLeafletMap,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(props) {
|
||||||
|
const store = useStore(),
|
||||||
|
canZoomIn = ref<boolean>(false),
|
||||||
|
canZoomOut = ref<boolean>(false);
|
||||||
|
|
||||||
|
const zoomIn = () => props.leaflet.zoomIn();
|
||||||
|
const zoomOut = () => props.leaflet.zoomOut();
|
||||||
|
|
||||||
|
const updateZoom = () => {
|
||||||
|
canZoomIn.value = props.leaflet.getZoom() < props.leaflet.getMaxZoom();
|
||||||
|
canZoomOut.value = props.leaflet.getZoom() > props.leaflet.getMinZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateZoom();
|
||||||
|
props.leaflet.on('zoom', updateZoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => props.leaflet.off('zoom', updateZoom));
|
||||||
|
|
||||||
|
return {
|
||||||
|
canZoomIn,
|
||||||
|
canZoomOut,
|
||||||
|
|
||||||
|
zoomIn,
|
||||||
|
zoomOut,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.zoom {
|
||||||
|
@media (max-width: 480px) and (pointer: coarse), (max-height: 480px) and (pointer: coarse), (max-height: 400px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__button {
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
text-indent: 0.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -21,11 +21,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core";
|
import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core";
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
|
||||||
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
|
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
|
||||||
import {LiveAtlasMarkerSet} from "@/index";
|
import {LiveAtlasMarkerSet} from "@/index";
|
||||||
import {watch} from "vue";
|
import {markRaw, watch} from "vue";
|
||||||
import Markers from "@/components/map/marker/Markers.vue";
|
import Markers from "@/components/map/marker/Markers.vue";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -33,11 +33,6 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
leaflet: {
|
|
||||||
type: Object as () => LiveAtlasLeafletMap,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
markerSet: {
|
markerSet: {
|
||||||
type: Object as () => LiveAtlasMarkerSet,
|
type: Object as () => LiveAtlasMarkerSet,
|
||||||
required: true,
|
required: true,
|
||||||
@ -65,27 +60,25 @@ export default defineComponent({
|
|||||||
priority: props.markerSet.priority,
|
priority: props.markerSet.priority,
|
||||||
});
|
});
|
||||||
|
|
||||||
if(newValue.hidden) {
|
// store.commit(MutationTypes.UPDATE_LAYER, {
|
||||||
props.leaflet.getLayerManager()
|
// layer: layerGroup,
|
||||||
.addHiddenLayer(layerGroup, newValue.label, props.markerSet.priority);
|
// options: {enabled: newValue.hidden}
|
||||||
} else {
|
// });
|
||||||
props.leaflet.getLayerManager()
|
|
||||||
.addLayer(layerGroup, true, newValue.label, props.markerSet.priority);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, {deep: true});
|
}, {deep: true});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(props.markerSet.hidden) {
|
store.commit(MutationTypes.ADD_LAYER, {
|
||||||
props.leaflet.getLayerManager()
|
layer: markRaw(layerGroup),
|
||||||
.addHiddenLayer(layerGroup, props.markerSet.label, props.markerSet.priority);
|
name: props.markerSet.label,
|
||||||
} else {
|
overlay: true,
|
||||||
props.leaflet.getLayerManager()
|
position: props.markerSet.priority || 0,
|
||||||
.addLayer(layerGroup, true, props.markerSet.label, props.markerSet.priority);
|
enabled: !props.markerSet.hidden,
|
||||||
}
|
showInControl: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
|
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
markerSettings,
|
markerSettings,
|
||||||
|
@ -24,6 +24,8 @@ import {defineComponent, computed, watch, onMounted, onUnmounted} from "@vue/run
|
|||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {LayerGroup} from 'leaflet';
|
import {LayerGroup} from 'leaflet';
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {markRaw} from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -51,21 +53,17 @@ export default defineComponent({
|
|||||||
watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150));
|
watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150));
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if(!componentSettings.value!.hideByDefault) {
|
store.commit(MutationTypes.ADD_LAYER, {
|
||||||
props.leaflet.getLayerManager().addLayer(
|
layer: markRaw(layerGroup),
|
||||||
layerGroup,
|
name: store.state.components.players.markers!.layerName,
|
||||||
true,
|
overlay: true,
|
||||||
store.state.components.players.markers!.layerName,
|
position: componentSettings.value!.layerPriority,
|
||||||
componentSettings.value!.layerPriority);
|
enabled: !componentSettings.value!.hideByDefault,
|
||||||
} else {
|
showInControl: true
|
||||||
props.leaflet.getLayerManager().addHiddenLayer(
|
});
|
||||||
layerGroup,
|
|
||||||
store.state.components.players.markers!.layerName,
|
|
||||||
componentSettings.value!.layerPriority);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
|
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
|
||||||
|
|
||||||
if(playersAboveMarkers.value) {
|
if(playersAboveMarkers.value) {
|
||||||
playerPane.style.zIndex = '600';
|
playerPane.style.zIndex = '600';
|
||||||
|
@ -19,6 +19,8 @@ import {defineComponent, onUnmounted} from "@vue/runtime-core";
|
|||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||||
import {LiveAtlasTileLayerOverlay} from "@/index";
|
import {LiveAtlasTileLayerOverlay} from "@/index";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {markRaw} from "vue";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
@ -37,10 +39,17 @@ export default defineComponent({
|
|||||||
|
|
||||||
let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options.tileLayerOptions))));
|
let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options.tileLayerOptions))));
|
||||||
|
|
||||||
props.leaflet.getLayerManager().addHiddenLayer(layer, props.options.label, props.options.priority);
|
store.commit(MutationTypes.ADD_LAYER, {
|
||||||
|
layer: markRaw(layer),
|
||||||
|
name: props.options.label,
|
||||||
|
overlay: true,
|
||||||
|
position: props.options.priority || 0,
|
||||||
|
enabled: false,
|
||||||
|
showInControl: true
|
||||||
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
props.leaflet.getLayerManager().removeLayer(layer);
|
store.commit(MutationTypes.REMOVE_LAYER, layer)
|
||||||
layer.remove();
|
layer.remove();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
42
src/index.d.ts
vendored
42
src/index.d.ts
vendored
@ -18,16 +18,14 @@ import {State} from "@/store";
|
|||||||
import {DynmapUrlConfig} from "@/dynmap";
|
import {DynmapUrlConfig} from "@/dynmap";
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||||
import {
|
import {
|
||||||
|
ControlOptions,
|
||||||
Coords,
|
Coords,
|
||||||
DoneCallback, FitBoundsOptions,
|
DoneCallback, FitBoundsOptions,
|
||||||
InternalTiles, LatLng,
|
InternalTiles, LatLng, Layer,
|
||||||
PathOptions,
|
PathOptions,
|
||||||
PointTuple,
|
PointTuple,
|
||||||
PolylineOptions
|
PolylineOptions
|
||||||
} from "leaflet";
|
} from "leaflet";
|
||||||
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
|
|
||||||
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
|
|
||||||
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
|
|
||||||
import {globalMessages, serverMessages} from "../messages";
|
import {globalMessages, serverMessages} from "../messages";
|
||||||
import {LiveAtlasMarkerType} from "@/util/markers";
|
import {LiveAtlasMarkerType} from "@/util/markers";
|
||||||
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||||
@ -173,6 +171,23 @@ interface LiveAtlasWorldState {
|
|||||||
timeOfDay: number;
|
timeOfDay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LiveAtlasLayerDefinition {
|
||||||
|
layer: Layer;
|
||||||
|
overlay: boolean;
|
||||||
|
name: string;
|
||||||
|
position: number;
|
||||||
|
enabled: boolean;
|
||||||
|
showInControl: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LiveAtlasPartialLayerDefinition {
|
||||||
|
overlay?: boolean;
|
||||||
|
name?: string;
|
||||||
|
position?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
showInControl?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface LiveAtlasParsedUrl {
|
interface LiveAtlasParsedUrl {
|
||||||
world?: string;
|
world?: string;
|
||||||
map?: string;
|
map?: string;
|
||||||
@ -291,6 +306,25 @@ interface LiveAtlasComponentConfig {
|
|||||||
login: boolean;
|
login: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClockControlOptions extends ControlOptions {
|
||||||
|
showTimeOfDay: boolean;
|
||||||
|
showDigitalClock: boolean;
|
||||||
|
showWeather: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoordinatesControlOptions extends ControlOptions {
|
||||||
|
showY: boolean;
|
||||||
|
showRegion: boolean;
|
||||||
|
showChunk: boolean;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogoControlOptions extends ControlOptions {
|
||||||
|
url?: string;
|
||||||
|
image?: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface LiveAtlasPartialComponentConfig {
|
interface LiveAtlasPartialComponentConfig {
|
||||||
markers?: {
|
markers?: {
|
||||||
showLabels: boolean;
|
showLabels: boolean;
|
||||||
|
@ -14,44 +14,14 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Map, DomUtil, MapOptions} from 'leaflet';
|
import {Map, MapOptions} from 'leaflet';
|
||||||
import LayerManager from "@/leaflet/layer/LayerManager";
|
|
||||||
|
|
||||||
export default class LiveAtlasLeafletMap extends Map {
|
export default class LiveAtlasLeafletMap extends Map {
|
||||||
declare _controlCorners: any;
|
declare _controlCorners: any;
|
||||||
declare _controlContainer?: HTMLElement;
|
declare _controlContainer?: HTMLElement;
|
||||||
declare _container?: HTMLElement;
|
declare _container?: HTMLElement;
|
||||||
|
|
||||||
private readonly _layerManager: LayerManager;
|
|
||||||
|
|
||||||
constructor(element: string | HTMLElement, options?: MapOptions) {
|
constructor(element: string | HTMLElement, options?: MapOptions) {
|
||||||
super(element, options);
|
super(element, options);
|
||||||
|
|
||||||
this._layerManager = Object.seal(new LayerManager(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
getLayerManager(): LayerManager {
|
|
||||||
return this._layerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
_initControlPos() {
|
|
||||||
const corners: any = this._controlCorners = {},
|
|
||||||
l = 'leaflet-',
|
|
||||||
container = this._controlContainer =
|
|
||||||
DomUtil.create('div', l + 'control-container', this._container);
|
|
||||||
|
|
||||||
function createCorner(vSide: string, hSide: string) {
|
|
||||||
const className = l + vSide + ' ' + l + hSide;
|
|
||||||
|
|
||||||
corners[`${vSide}${hSide}`] = DomUtil.create('div', className, container);
|
|
||||||
}
|
|
||||||
|
|
||||||
createCorner('top', 'left');
|
|
||||||
createCorner('top', 'right');
|
|
||||||
createCorner('top', 'center');
|
|
||||||
createCorner('bottom', 'center');
|
|
||||||
createCorner('bottom', 'left');
|
|
||||||
createCorner('bottom', 'right');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Control, ControlOptions, DomEvent, DomUtil} from 'leaflet';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {MutationTypes} from "@/store/mutation-types";
|
|
||||||
import {watch} from "@vue/runtime-core";
|
|
||||||
|
|
||||||
import "@/assets/icons/chat.svg";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control providing a chat button which opens the chatbox on click
|
|
||||||
*/
|
|
||||||
export class ChatControl extends Control {
|
|
||||||
declare options: ControlOptions
|
|
||||||
|
|
||||||
constructor(options: ControlOptions) {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
const store = useStore(),
|
|
||||||
chatButton = DomUtil.create('button',
|
|
||||||
'leaflet-control-bottom leaflet-control-button leaflet-control-chat') as HTMLButtonElement;
|
|
||||||
|
|
||||||
chatButton.type = 'button';
|
|
||||||
chatButton.title = store.state.messages.chatTitle;
|
|
||||||
chatButton.innerHTML = `
|
|
||||||
<svg class="svg-icon">
|
|
||||||
<use xlink:href="#icon--chat" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
chatButton.addEventListener('click', e => {
|
|
||||||
store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'chat');
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Open chat on ArrowRight from button
|
|
||||||
DomEvent.on(chatButton,'keydown', (e: Event) => {
|
|
||||||
if((e as KeyboardEvent).key === 'ArrowRight') {
|
|
||||||
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'chat', state: true});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(store.state.ui.visibleElements, (newValue) => {
|
|
||||||
chatButton.setAttribute('aria-expanded', newValue.has('chat').toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
return chatButton;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,175 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
||||||
* These portions are Copyright 2020 Dynmap Contributors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {ControlOptions, DomUtil, Util, Control} from 'leaflet';
|
|
||||||
import {getMinecraftTime} from '@/util';
|
|
||||||
|
|
||||||
import {watch} from "@vue/runtime-core";
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
|
|
||||||
import "@/assets/icons/clock_moon.svg";
|
|
||||||
import "@/assets/icons/clock_moon_rain.svg";
|
|
||||||
import "@/assets/icons/clock_moon_storm.svg";
|
|
||||||
import "@/assets/icons/clock_sun.svg";
|
|
||||||
import "@/assets/icons/clock_sun_rain.svg";
|
|
||||||
import "@/assets/icons/clock_sun_storm.svg";
|
|
||||||
import {LiveAtlasWorldState} from "@/index";
|
|
||||||
|
|
||||||
export interface ClockControlOptions extends ControlOptions {
|
|
||||||
showTimeOfDay: boolean;
|
|
||||||
showDigitalClock: boolean;
|
|
||||||
showWeather: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control providing a clock which can display the current in-game time of day and weather
|
|
||||||
*/
|
|
||||||
export class ClockControl extends Control {
|
|
||||||
declare options: ClockControlOptions;
|
|
||||||
declare _container?: HTMLElement;
|
|
||||||
|
|
||||||
private _sun?: HTMLElement;
|
|
||||||
private _moon?: HTMLElement;
|
|
||||||
private _clock?: HTMLElement;
|
|
||||||
private _currentMoonIcon?: string;
|
|
||||||
private _currentSunIcon?: string;
|
|
||||||
private _unwatchHandler?: Function;
|
|
||||||
|
|
||||||
constructor(options: ClockControlOptions) {
|
|
||||||
super(Object.assign(options, {position: 'topcenter'}));
|
|
||||||
|
|
||||||
Util.setOptions(this, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
const digital = !this.options.showTimeOfDay && !this.options.showWeather && this.options.showDigitalClock,
|
|
||||||
worldState = useStore().state.currentWorldState;
|
|
||||||
|
|
||||||
this._container = DomUtil.create('div', 'clock' + (digital ? ' clock--digital' : ''));
|
|
||||||
this._sun = DomUtil.create('div', 'clock__sun', this._container);
|
|
||||||
this._moon = DomUtil.create('div', 'clock__moon', this._container);
|
|
||||||
|
|
||||||
this._sun.style.transform = 'translate(-150px, -150px)';
|
|
||||||
this._moon.style.transform = 'translate(-150px, -150px)';
|
|
||||||
|
|
||||||
this._sun!.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--clock_sun" />
|
|
||||||
</svg>`;
|
|
||||||
this._moon!.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--clock_moon" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
if (this.options.showDigitalClock) {
|
|
||||||
this._clock = DomUtil.create('div', 'clock__time', this._container)
|
|
||||||
}
|
|
||||||
|
|
||||||
this._unwatchHandler = watch(worldState, (newValue) => {
|
|
||||||
this._update(newValue);
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
return this._container;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove() {
|
|
||||||
if(this._unwatchHandler) {
|
|
||||||
this._unwatchHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_update(worldState: LiveAtlasWorldState) {
|
|
||||||
const timeOfDay = worldState.timeOfDay;
|
|
||||||
let sunAngle;
|
|
||||||
|
|
||||||
if (timeOfDay > 23100 || timeOfDay < 12900) {
|
|
||||||
//day mode
|
|
||||||
let movedTime = timeOfDay + 900;
|
|
||||||
movedTime = (movedTime >= 24000) ? movedTime - 24000 : movedTime;
|
|
||||||
//Now we have 0 -> 13800 for the day period
|
|
||||||
//Divide by 13800*2=27600 instead of 24000 to compress day
|
|
||||||
sunAngle = ((movedTime) / 27600 * 2 * Math.PI);
|
|
||||||
} else {
|
|
||||||
//night mode
|
|
||||||
const movedTime = timeOfDay - 12900;
|
|
||||||
//Now we have 0 -> 10200 for the night period
|
|
||||||
//Divide by 10200*2=20400 instead of 24000 to expand night
|
|
||||||
sunAngle = Math.PI + ((movedTime) / 20400 * 2 * Math.PI);
|
|
||||||
}
|
|
||||||
|
|
||||||
const moonAngle = sunAngle + Math.PI;
|
|
||||||
|
|
||||||
if (timeOfDay >= 0) {
|
|
||||||
this._sun!.style.transform = 'translate(' + Math.round(-50 * Math.cos(sunAngle)) + 'px, ' + Math.round(-50 * Math.sin(sunAngle)) + 'px)';
|
|
||||||
this._moon!.style.transform = 'translate(' + Math.round(-50 * Math.cos(moonAngle)) + 'px, ' + Math.round(-50 * Math.sin(moonAngle)) + 'px)';
|
|
||||||
} else {
|
|
||||||
this._sun!.style.transform = 'translate(-150px, -150px)';
|
|
||||||
this._moon!.style.transform = 'translate(-150px, -150px)';
|
|
||||||
}
|
|
||||||
|
|
||||||
const minecraftTime = getMinecraftTime(timeOfDay);
|
|
||||||
|
|
||||||
if (this.options.showDigitalClock) {
|
|
||||||
if (timeOfDay >= 0) {
|
|
||||||
this._clock!.classList.remove(minecraftTime.night ? 'day' : 'night');
|
|
||||||
this._clock!.classList.add(minecraftTime.day ? 'day' : 'night');
|
|
||||||
this._clock!.textContent = [
|
|
||||||
minecraftTime.hours.toString().padStart(2, '0'),
|
|
||||||
minecraftTime.minutes.toString().padStart(2, '0')
|
|
||||||
].join(':');
|
|
||||||
} else {
|
|
||||||
this._clock!.classList.remove(minecraftTime.night ? 'day' : 'night');
|
|
||||||
this._clock!.textContent = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showWeather) {
|
|
||||||
if (worldState.thundering) {
|
|
||||||
this._setSunIcon('clock_sun_storm');
|
|
||||||
this._setMoonIcon('clock_moon_storm');
|
|
||||||
} else if (worldState.raining) {
|
|
||||||
this._setSunIcon('clock_sun_rain');
|
|
||||||
this._setMoonIcon('clock_moon_rain');
|
|
||||||
} else {
|
|
||||||
this._setSunIcon('clock_sun');
|
|
||||||
this._setMoonIcon('clock_moon');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setSunIcon(icon: string) {
|
|
||||||
if(this._sun && this._currentSunIcon !== icon) {
|
|
||||||
this._sun!.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--${icon}" />
|
|
||||||
</svg>`;
|
|
||||||
this._currentSunIcon = icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_setMoonIcon(icon: string) {
|
|
||||||
if(this._moon && this._currentMoonIcon !== icon) {
|
|
||||||
this._moon!.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--${icon}" />
|
|
||||||
</svg>`;
|
|
||||||
this._currentMoonIcon = icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,167 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
||||||
* These portions are Copyright 2020 Dynmap Contributors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {ControlOptions, LeafletMouseEvent, Control, Map, DomUtil, Util} from 'leaflet';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {Coordinate} from "@/index";
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
export interface CoordinatesControlOptions extends ControlOptions {
|
|
||||||
showY: boolean;
|
|
||||||
showRegion: boolean;
|
|
||||||
showChunk: boolean;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control which displays in-game block coordinates when hovering over or tapping the map
|
|
||||||
*/
|
|
||||||
export class CoordinatesControl extends Control {
|
|
||||||
declare options: CoordinatesControlOptions;
|
|
||||||
declare _map ?: Map;
|
|
||||||
|
|
||||||
private _location?: Coordinate;
|
|
||||||
private _locationChanged: boolean = false;
|
|
||||||
private readonly _coordsContainer: HTMLSpanElement;
|
|
||||||
private readonly _regionContainer: HTMLSpanElement;
|
|
||||||
private readonly _chunkContainer: HTMLSpanElement;
|
|
||||||
|
|
||||||
constructor(options: CoordinatesControlOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this._coordsContainer = DomUtil.create('span', 'value coordinates');
|
|
||||||
this._chunkContainer = DomUtil.create('span', 'value chunk');
|
|
||||||
this._regionContainer = DomUtil.create('span', 'value region');
|
|
||||||
|
|
||||||
options.position = 'bottomleft';
|
|
||||||
Util.setOptions(this, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(map: Map) {
|
|
||||||
const container = DomUtil.create('div', 'leaflet-control-coordinates');
|
|
||||||
|
|
||||||
this._coordsContainer.textContent = this.options.showY ? '-----, ---, -----' : '-----, -----';
|
|
||||||
this._coordsContainer.dataset.label = this.options.label;
|
|
||||||
container.appendChild(this._coordsContainer);
|
|
||||||
|
|
||||||
if (this.options.showRegion) {
|
|
||||||
this._regionContainer.textContent = '--------------';
|
|
||||||
this._regionContainer.dataset.label = store.state.messages.locationRegion;
|
|
||||||
container.appendChild(this._regionContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showChunk) {
|
|
||||||
this._chunkContainer.textContent = '----, ----';
|
|
||||||
this._chunkContainer.dataset.label = store.state.messages.locationChunk;
|
|
||||||
container.appendChild(this._chunkContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
map.on('mousemove', this._onMouseMove, this);
|
|
||||||
map.on('mouseout', this._onMouseOut, this);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
if (!this._map) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._map.off('mousemove', this._onMouseMove, this);
|
|
||||||
this._map.off('mouseout', this._onMouseOut, this);
|
|
||||||
super.remove();
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseMove(event: LeafletMouseEvent) {
|
|
||||||
if (!this._map || !store.state.currentMap) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._location = store.state.currentMap.latLngToLocation(event.latlng, store.state.currentWorld!.seaLevel + 1);
|
|
||||||
|
|
||||||
if(!this._locationChanged) {
|
|
||||||
this._locationChanged = true;
|
|
||||||
requestAnimationFrame(() => this._update());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onMouseOut() {
|
|
||||||
if (!this._map) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._location = undefined;
|
|
||||||
|
|
||||||
if(!this._locationChanged) {
|
|
||||||
this._locationChanged = true;
|
|
||||||
requestAnimationFrame(() => this._update());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_update() {
|
|
||||||
if (!this._map || !store.state.currentWorld || !this._locationChanged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._locationChanged = false;
|
|
||||||
|
|
||||||
if(!this._location) {
|
|
||||||
if (this.options.showY) {
|
|
||||||
this._coordsContainer.textContent = '-----, ---, -----';
|
|
||||||
} else {
|
|
||||||
this._coordsContainer.textContent = '-----, -----';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showRegion) {
|
|
||||||
this._regionContainer.textContent = '--------------';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showChunk) {
|
|
||||||
this._chunkContainer.textContent = '----, ----';
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x = Math.round(this._location.x).toString().padStart(5, ' '),
|
|
||||||
y = this._location.y.toString().padStart(3, ' '),
|
|
||||||
z = Math.round(this._location.z).toString().padStart(5, ' '),
|
|
||||||
regionX = Math.floor(this._location.x / 512).toString().padStart(3, ' '),
|
|
||||||
regionZ = Math.floor(this._location.z / 512).toString().padStart(3, ' '),
|
|
||||||
chunkX = Math.floor(this._location.x / 16).toString().padStart(4, ' '),
|
|
||||||
chunkZ = Math.floor(this._location.z / 16).toString().padStart(4, ' ');
|
|
||||||
|
|
||||||
if (this.options.showY) {
|
|
||||||
this._coordsContainer.textContent = `${x}, ${y}, ${z}`;
|
|
||||||
} else {
|
|
||||||
this._coordsContainer.textContent = `${x}, ${z}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showRegion) {
|
|
||||||
this._regionContainer.textContent = `r.${regionX}, ${regionZ}.mca`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.showChunk) {
|
|
||||||
this._chunkContainer.textContent = `${chunkX}, ${chunkZ}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
||||||
* These portions are Copyright 2020 Dynmap Contributors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Control, ControlOptions, DomUtil} from 'leaflet';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import '@/assets/icons/link.svg';
|
|
||||||
import { toClipboard } from '@soerenmartius/vue3-clipboard';
|
|
||||||
import {clipboardError, clipboardSuccess} from "@/util";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control providing a button for copying a link for the current map view to the clipboard
|
|
||||||
*/
|
|
||||||
export class LinkControl extends Control {
|
|
||||||
declare options: ControlOptions
|
|
||||||
|
|
||||||
constructor(options: ControlOptions) {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
const store = useStore(),
|
|
||||||
linkButton = DomUtil.create('button',
|
|
||||||
'leaflet-control-button leaflet-control-link') as HTMLButtonElement,
|
|
||||||
copySuccess = clipboardSuccess(store),
|
|
||||||
copyError = clipboardError(store);
|
|
||||||
|
|
||||||
linkButton.type = 'button';
|
|
||||||
linkButton.title = store.state.messages.linkTitle;
|
|
||||||
linkButton.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--link" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
linkButton.addEventListener('click', e => {
|
|
||||||
e.preventDefault();
|
|
||||||
toClipboard(window.location.href.split("#")[0] + store.getters.url)
|
|
||||||
.then(copySuccess)
|
|
||||||
.catch(copyError)
|
|
||||||
});
|
|
||||||
|
|
||||||
return linkButton;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,235 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
||||||
* These portions are Copyright 2020 Dynmap Contributors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Control, DomEvent, DomUtil, Layer, LeafletEvent, Map as LeafletMap, Util} from 'leaflet';
|
|
||||||
|
|
||||||
import '@/assets/icons/layers.svg';
|
|
||||||
import '@/assets/icons/checkbox.svg';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {MutationTypes} from "@/store/mutation-types";
|
|
||||||
import {nextTick, watch} from "vue";
|
|
||||||
import {handleKeyboardEvent} from "@/util/events";
|
|
||||||
import LayersObject = Control.LayersObject;
|
|
||||||
import LayersOptions = Control.LayersOptions;
|
|
||||||
|
|
||||||
const store = useStore();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extension of leaflet's standard {@link Control.Layers}
|
|
||||||
* Sorts layers by position, adds additional keyboard navigation, adjusts to viewport size and tracks expanded state in vuex
|
|
||||||
*/
|
|
||||||
export class LiveAtlasLayerControl extends Control.Layers {
|
|
||||||
declare _map ?: LeafletMap;
|
|
||||||
declare _overlaysList?: HTMLElement;
|
|
||||||
declare _baseLayersList?: HTMLElement;
|
|
||||||
declare _layerControlInputs?: HTMLElement[];
|
|
||||||
declare _container?: HTMLElement;
|
|
||||||
declare _section?: HTMLElement;
|
|
||||||
declare _separator?: HTMLElement;
|
|
||||||
|
|
||||||
private _layersButton?: HTMLElement;
|
|
||||||
private _layerPositions: Map<Layer, number>;
|
|
||||||
private visible: boolean = false;
|
|
||||||
|
|
||||||
constructor(baseLayers?: LayersObject, overlays?: LayersObject, options?: LayersOptions) {
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
super(baseLayers, overlays, Object.assign(options || {}, {
|
|
||||||
sortLayers: true,
|
|
||||||
sortFunction: (layer1: Layer, layer2: Layer, name1: string, name2: string) => {
|
|
||||||
const priority1 = this._layerPositions.get(layer1) || 0,
|
|
||||||
priority2 = this._layerPositions.get(layer2) || 0;
|
|
||||||
|
|
||||||
if(priority1 !== priority2) {
|
|
||||||
return priority1 - priority2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ((name1 < name2) ? -1 : ((name1 > name2) ? 1 : 0));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
this._layerPositions = new Map<Layer, number>();
|
|
||||||
}
|
|
||||||
|
|
||||||
hasLayer(layer: Layer): boolean {
|
|
||||||
// @ts-ignore
|
|
||||||
return !!super._getLayer(Util.stamp(layer));
|
|
||||||
}
|
|
||||||
|
|
||||||
expand() {
|
|
||||||
this._layersButton!.setAttribute('aria-expanded', 'true');
|
|
||||||
this._section!.style.display = '';
|
|
||||||
this.handleResize();
|
|
||||||
|
|
||||||
const firstCheckbox = this._container!.querySelector('input');
|
|
||||||
|
|
||||||
if(firstCheckbox) {
|
|
||||||
(firstCheckbox as HTMLElement).focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
super._checkDisabledLayers();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
collapse() {
|
|
||||||
this._layersButton!.setAttribute('aria-expanded', 'false');
|
|
||||||
this._section!.style.display = 'none';
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
_initLayout() {
|
|
||||||
const className = 'leaflet-control-layers',
|
|
||||||
container = this._container = DomUtil.create('div', className),
|
|
||||||
section = this._section = DomUtil.create('section', className + '-list'),
|
|
||||||
button = this._layersButton = DomUtil.create('button', className + '-toggle', container);
|
|
||||||
|
|
||||||
DomEvent.disableClickPropagation(container);
|
|
||||||
DomEvent.disableScrollPropagation(container);
|
|
||||||
|
|
||||||
//Open layer list on ArrowRight from button
|
|
||||||
DomEvent.on(button,'keydown', (e: Event) => {
|
|
||||||
if((e as KeyboardEvent).key === 'ArrowRight') {
|
|
||||||
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: true});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
DomEvent.on(container, 'keydown', (e: Event) => {
|
|
||||||
//Close layer list on ArrowLeft from within list
|
|
||||||
if((e as KeyboardEvent).key === 'ArrowLeft') {
|
|
||||||
e.preventDefault();
|
|
||||||
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: false});
|
|
||||||
nextTick(() => button.focus());
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = Array.from(container.querySelectorAll('input')) as HTMLElement[];
|
|
||||||
handleKeyboardEvent(e as KeyboardEvent, elements);
|
|
||||||
});
|
|
||||||
DomEvent.on(button,'click', () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'layers'));
|
|
||||||
|
|
||||||
section.style.display = 'none';
|
|
||||||
|
|
||||||
button.title = store.state.messages.layersTitle;
|
|
||||||
button.setAttribute('aria-expanded', 'false');
|
|
||||||
button.innerHTML = `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--layers" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
|
|
||||||
//Use vuex track expanded state
|
|
||||||
watch(store.state.ui.visibleElements, (newValue) => {
|
|
||||||
if(newValue.has('layers') && !this.visible) {
|
|
||||||
this.expand();
|
|
||||||
} else if(this.visible && !newValue.has('layers')) {
|
|
||||||
this.collapse();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visible = store.state.ui.visibleElements.has('layers');
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(store.state.messages, (newValue) => (button.title = newValue.layersTitle));//
|
|
||||||
|
|
||||||
this.visible = store.state.ui.visibleElements.has('layers');
|
|
||||||
|
|
||||||
if (this.visible) {
|
|
||||||
this.expand();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._baseLayersList = DomUtil.create('div', className + '-base', section);
|
|
||||||
this._separator = DomUtil.create('div', className + '-separator', section);
|
|
||||||
this._overlaysList = DomUtil.create('div', className + '-overlays', section);
|
|
||||||
|
|
||||||
container.appendChild(section);
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => this.handleResize());
|
|
||||||
this.handleResize();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleResize() {
|
|
||||||
const y = this._layersButton!.getBoundingClientRect().y;
|
|
||||||
|
|
||||||
//Limit height to remaining vertical space
|
|
||||||
// Including 30px element padding, 10px padding from edge of viewport, and 55px padding to avoid covering bottom bar
|
|
||||||
this._section!.style.maxHeight = `calc(100vh - ${(y + 30 + 10 + 55)}px)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
addOverlayAtPosition(layer: Layer, name: string, position: number): this {
|
|
||||||
this._layerPositions.set(layer, position);
|
|
||||||
return super.addOverlay(layer, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
addOverlay(layer: Layer, name: string): this {
|
|
||||||
this._layerPositions.set(layer, 0);
|
|
||||||
return super.addOverlay(layer, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
removeLayer(layer: Layer): this {
|
|
||||||
this._layerPositions.delete(layer);
|
|
||||||
return super.removeLayer(layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
_addItem(obj: any) {
|
|
||||||
const container = obj.overlay ? this._overlaysList : this._baseLayersList,
|
|
||||||
item = document.createElement('label'),
|
|
||||||
label = document.createElement('span'),
|
|
||||||
checked = this._map!.hasLayer(obj.layer);
|
|
||||||
|
|
||||||
let input;
|
|
||||||
|
|
||||||
item.className = 'layer checkbox';
|
|
||||||
|
|
||||||
if (obj.overlay) {
|
|
||||||
input = document.createElement('input');
|
|
||||||
input.type = 'checkbox';
|
|
||||||
input.className = 'leaflet-control-layers-selector';
|
|
||||||
input.defaultChecked = checked;
|
|
||||||
} else {
|
|
||||||
// @ts-ignore
|
|
||||||
input = super._createRadioElement('leaflet-base-layers_' + Util.stamp(this), checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
input.layerId = Util.stamp(obj.layer);
|
|
||||||
this._layerControlInputs!.push(input);
|
|
||||||
label.textContent = obj.name;
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
DomEvent.on(input, 'click', (e: LeafletEvent) => super._onInputClick(e), this);
|
|
||||||
|
|
||||||
item.appendChild(input);
|
|
||||||
item.insertAdjacentHTML('beforeend', `
|
|
||||||
<svg class="svg-icon" aria-hidden="true">
|
|
||||||
<use xlink:href="#icon--checkbox" />
|
|
||||||
</svg>`);
|
|
||||||
item.appendChild(label);
|
|
||||||
|
|
||||||
container!.appendChild(item);
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
super._checkDisabledLayers();
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(map: LeafletMap) {
|
|
||||||
this._layerControlInputs = [];
|
|
||||||
|
|
||||||
(super.onRemove as Function)(map);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
/*
|
|
||||||
Portions of this file are taken from Leaflet.loading:
|
|
||||||
|
|
||||||
Copyright (c) 2013 Eric Brelsford
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Control,
|
|
||||||
ControlOptions,
|
|
||||||
DomUtil,
|
|
||||||
Layer,
|
|
||||||
LeafletEvent,
|
|
||||||
Map, TileLayer,
|
|
||||||
} from 'leaflet';
|
|
||||||
import '@/assets/icons/loading.svg';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
|
|
||||||
export interface LoadingControlOptions extends ControlOptions {
|
|
||||||
delayIndicator?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control which displays a loading spinner when any tiles are loading
|
|
||||||
*/
|
|
||||||
export class LoadingControl extends Control {
|
|
||||||
declare options: LoadingControlOptions;
|
|
||||||
|
|
||||||
private _dataLoaders: Set<number> = new Set();
|
|
||||||
private readonly _loadingIndicator: HTMLDivElement;
|
|
||||||
private _delayIndicatorTimeout: null | ReturnType<typeof setTimeout> = null;
|
|
||||||
|
|
||||||
constructor(options: LoadingControlOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this._loadingIndicator = DomUtil.create('div',
|
|
||||||
'leaflet-control-button leaflet-control-loading') as HTMLDivElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd(map: Map) {
|
|
||||||
this._loadingIndicator.title = useStore().state.messages.loadingTitle;
|
|
||||||
this._loadingIndicator.hidden = true;
|
|
||||||
this._loadingIndicator.innerHTML = `
|
|
||||||
<svg class="svg-icon">
|
|
||||||
<use xlink:href="#icon--loading" />
|
|
||||||
</svg>`;
|
|
||||||
|
|
||||||
this._addLayerListeners(map);
|
|
||||||
this._addMapListeners(map);
|
|
||||||
|
|
||||||
return this._loadingIndicator;
|
|
||||||
}
|
|
||||||
|
|
||||||
onRemove(map: Map) {
|
|
||||||
this._removeLayerListeners(map);
|
|
||||||
this._removeMapListeners(map);
|
|
||||||
}
|
|
||||||
|
|
||||||
addLoader(id: number) {
|
|
||||||
this._dataLoaders.add(id);
|
|
||||||
|
|
||||||
if (this.options.delayIndicator && !this._delayIndicatorTimeout) {
|
|
||||||
// If we are delaying showing the indicator and we're not
|
|
||||||
// already waiting for that delay, set up a timeout.
|
|
||||||
this._delayIndicatorTimeout = setTimeout(() => {
|
|
||||||
this.updateIndicator();
|
|
||||||
this._delayIndicatorTimeout = null;
|
|
||||||
}, this.options.delayIndicator);
|
|
||||||
} else {
|
|
||||||
// Otherwise show the indicator immediately
|
|
||||||
this.updateIndicator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeLoader(id: number) {
|
|
||||||
this._dataLoaders.delete(id);
|
|
||||||
this.updateIndicator();
|
|
||||||
|
|
||||||
// If removing this loader means we're in no danger of loading,
|
|
||||||
// clear the timeout. This prevents old delays from instantly
|
|
||||||
// triggering the indicator.
|
|
||||||
if (this.options.delayIndicator && this._delayIndicatorTimeout && !this.isLoading()) {
|
|
||||||
clearTimeout(this._delayIndicatorTimeout);
|
|
||||||
this._delayIndicatorTimeout = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIndicator() {
|
|
||||||
if (this.isLoading()) {
|
|
||||||
this._showIndicator();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this._hideIndicator();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading() {
|
|
||||||
return this._countLoaders() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
_countLoaders() {
|
|
||||||
return this._dataLoaders.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
_showIndicator() {
|
|
||||||
this._loadingIndicator.hidden = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_hideIndicator() {
|
|
||||||
this._loadingIndicator.hidden = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleLoading(e: LeafletEvent) {
|
|
||||||
this.addLoader(this.getEventId(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleBaseLayerChange (e: LeafletEvent) {
|
|
||||||
// Check for a target 'layer' that contains multiple layers, such as
|
|
||||||
// L.LayerGroup. This will happen if you have an L.LayerGroup in an
|
|
||||||
// L.Control.Layers.
|
|
||||||
if (e.layer && e.layer.eachLayer && typeof e.layer.eachLayer === 'function') {
|
|
||||||
e.layer.eachLayer((layer: Layer) => {
|
|
||||||
this._handleBaseLayerChange({ layer: layer } as LeafletEvent);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleLoad(e: LeafletEvent) {
|
|
||||||
this.removeLoader(this.getEventId(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
getEventId(e: any) {
|
|
||||||
if (e.id) {
|
|
||||||
return e.id;
|
|
||||||
} else if (e.layer) {
|
|
||||||
return e.layer._leaflet_id;
|
|
||||||
}
|
|
||||||
return e.target._leaflet_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
_layerAdd(e: LeafletEvent) {
|
|
||||||
if(!(e.layer instanceof TileLayer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if(e.layer.isLoading()) {
|
|
||||||
this.addLoader((e.layer as any)._leaflet_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.layer.on('loading', this._handleLoading, this);
|
|
||||||
e.layer.on('load', this._handleLoad, this);
|
|
||||||
} catch (exception) {
|
|
||||||
console.warn('L.Control.Loading: Tried and failed to add ' +
|
|
||||||
' event handlers to layer', e.layer);
|
|
||||||
console.warn('L.Control.Loading: Full details', exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_layerRemove(e: LeafletEvent) {
|
|
||||||
if(!(e.layer instanceof TileLayer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
e.layer.off('loading', this._handleLoading, this);
|
|
||||||
e.layer.off('load', this._handleLoad, this);
|
|
||||||
} catch (exception) {
|
|
||||||
console.warn('L.Control.Loading: Tried and failed to remove ' +
|
|
||||||
'event handlers from layer', e.layer);
|
|
||||||
console.warn('L.Control.Loading: Full details', exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_addLayerListeners(map: Map) {
|
|
||||||
// Add listeners for begin and end of load to any layers already
|
|
||||||
// on the map
|
|
||||||
map.eachLayer((layer: Layer) => {
|
|
||||||
if(!(layer instanceof TileLayer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(layer.isLoading()) {
|
|
||||||
this.addLoader((layer as any)._leaflet_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.on('loading', this._handleLoading, this);
|
|
||||||
layer.on('load', this._handleLoad, this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// When a layer is added to the map, add listeners for begin and
|
|
||||||
// end of load
|
|
||||||
map.on('layeradd', this._layerAdd, this);
|
|
||||||
map.on('layerremove', this._layerRemove, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeLayerListeners(map: Map) {
|
|
||||||
// Remove listeners for begin and end of load from all layers
|
|
||||||
map.eachLayer((layer: Layer) => {
|
|
||||||
if(!(layer instanceof TileLayer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removeLoader((layer as any)._leaflet_id);
|
|
||||||
|
|
||||||
layer.off('loading', this._handleLoading, this);
|
|
||||||
layer.off('load', this._handleLoad, this);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove layeradd/layerremove listener from map
|
|
||||||
map.off('layeradd', this._layerAdd, this);
|
|
||||||
map.off('layerremove', this._layerRemove, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_addMapListeners(map: Map) {
|
|
||||||
// Add listeners to the map for (custom) dataloading and dataload
|
|
||||||
// events, eg, for AJAX calls that affect the map but will not be
|
|
||||||
// reflected in the above layer events.
|
|
||||||
map.on('baselayerchange', this._handleBaseLayerChange, this);
|
|
||||||
map.on('dataloading', this._handleLoading, this);
|
|
||||||
map.on('dataload', this._handleLoad, this);
|
|
||||||
map.on('layerremove', this._handleLoad, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_removeMapListeners(map: Map) {
|
|
||||||
map.off('baselayerchange', this._handleBaseLayerChange, this);
|
|
||||||
map.off('dataloading', this._handleLoading, this);
|
|
||||||
map.off('dataload', this._handleLoad, this);
|
|
||||||
map.off('layerremove', this._handleLoad, this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Control, ControlOptions, DomEvent, DomUtil} from 'leaflet';
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {watch} from "@vue/runtime-core";
|
|
||||||
|
|
||||||
import "@/assets/icons/login.svg";
|
|
||||||
import "@/assets/icons/logout.svg";
|
|
||||||
import {computed} from "vue";
|
|
||||||
import {ActionTypes} from "@/store/action-types";
|
|
||||||
import {notify} from "@kyvg/vue3-notification";
|
|
||||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control providing a login/logout button which opens the login modal/logs out on click
|
|
||||||
*/
|
|
||||||
export class LoginControl extends Control {
|
|
||||||
declare _map: LiveAtlasLeafletMap;
|
|
||||||
declare options: ControlOptions;
|
|
||||||
|
|
||||||
private readonly store = useStore();
|
|
||||||
private readonly loggedIn = computed(() => this.store.state.loggedIn);
|
|
||||||
private readonly _button: HTMLButtonElement;
|
|
||||||
|
|
||||||
constructor(options: ControlOptions) {
|
|
||||||
super(options);
|
|
||||||
|
|
||||||
this._button = DomUtil.create('button',
|
|
||||||
'leaflet-control-bottom leaflet-control-button leaflet-control-login') as HTMLButtonElement;
|
|
||||||
|
|
||||||
this._button.type = 'button';
|
|
||||||
|
|
||||||
this._button.addEventListener('click', async e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
await this.handleClick();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Open login on ArrowRight from button
|
|
||||||
DomEvent.on(this._button,'keydown', async (e: Event) => {
|
|
||||||
if ((e as KeyboardEvent).key === 'ArrowRight') {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
await this.handleClick();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(this.loggedIn, () => {
|
|
||||||
this.update();
|
|
||||||
});
|
|
||||||
|
|
||||||
const visibleModal = computed(() => this.store.state.ui.visibleModal);
|
|
||||||
|
|
||||||
watch(visibleModal, (newValue, oldValue) => {
|
|
||||||
this._button.setAttribute('aria-expanded', (newValue === 'login').toString());
|
|
||||||
|
|
||||||
if(this._map && !newValue && oldValue === 'login') {
|
|
||||||
this._button.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
return this._button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private update() {
|
|
||||||
this._button.title = this.loggedIn.value
|
|
||||||
? this.store.state.messages.logoutTitle : this.store.state.messages.loginTitle;
|
|
||||||
this._button.innerHTML = `
|
|
||||||
<svg class="svg-icon">
|
|
||||||
<use xlink:href="#icon--${this.loggedIn.value ? 'logout' : 'login'}" />
|
|
||||||
</svg>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleClick() {
|
|
||||||
const logoutSuccess = computed(() => this.store.state.messages.logoutSuccess),
|
|
||||||
logoutError = computed(() => this.store.state.messages.logoutErrorUnknown);
|
|
||||||
|
|
||||||
if (this.loggedIn.value) {
|
|
||||||
try {
|
|
||||||
await this.store.dispatch(ActionTypes.LOGOUT, undefined);
|
|
||||||
notify(logoutSuccess.value);
|
|
||||||
} catch(e) {
|
|
||||||
notify(logoutError.value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.store.dispatch(ActionTypes.LOGIN, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
||||||
* These portions are Copyright 2020 Dynmap Contributors.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Control, ControlOptions, DomUtil} from 'leaflet';
|
|
||||||
|
|
||||||
export interface LogoControlOptions extends ControlOptions {
|
|
||||||
url?: string;
|
|
||||||
image?: string;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Leaflet map control which displays an arbitrary image or text with an optional link
|
|
||||||
* Intended for use for dynmap logo components
|
|
||||||
*/
|
|
||||||
export class LogoControl extends Control {
|
|
||||||
declare options: LogoControlOptions;
|
|
||||||
|
|
||||||
constructor(options: LogoControlOptions) {
|
|
||||||
super(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
onAdd() {
|
|
||||||
const container = DomUtil.create('div', 'leaflet-control-logo');
|
|
||||||
let link;
|
|
||||||
|
|
||||||
if (this.options.url) {
|
|
||||||
link = DomUtil.create('a', '', container) as HTMLAnchorElement;
|
|
||||||
link.href = this.options.url;
|
|
||||||
link.setAttribute('aria-label', this.options.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.options.image) {
|
|
||||||
const image = DomUtil.create('img', '', link) as HTMLImageElement;
|
|
||||||
image.src = this.options.image;
|
|
||||||
image.alt = this.options.text;
|
|
||||||
} else {
|
|
||||||
container.textContent = this.options.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2022 James Lyne
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {Map, Layer} from 'leaflet';
|
|
||||||
import {LiveAtlasLayerControl} from "@/leaflet/control/LiveAtlasLayerControl";
|
|
||||||
import {watch} from "vue";
|
|
||||||
import {useStore} from "@/store";
|
|
||||||
import {computed} from "@vue/runtime-core";
|
|
||||||
|
|
||||||
export default class LayerManager {
|
|
||||||
private readonly layerControl: LiveAtlasLayerControl;
|
|
||||||
private readonly map: Map;
|
|
||||||
|
|
||||||
constructor(map: Map) {
|
|
||||||
const showControl = computed(() => useStore().state.components.layerControl);
|
|
||||||
this.map = map;
|
|
||||||
this.layerControl = new LiveAtlasLayerControl({}, {},{
|
|
||||||
position: 'topleft',
|
|
||||||
});
|
|
||||||
|
|
||||||
if(showControl.value) {
|
|
||||||
this.map.addControl(this.layerControl);
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(showControl, (show) => {
|
|
||||||
if(show) {
|
|
||||||
this.map.addControl(this.layerControl);
|
|
||||||
} else {
|
|
||||||
this.map.removeControl(this.layerControl);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
addLayer(layer: Layer, showInControl: boolean, name: string, position: number) {
|
|
||||||
this.map.addLayer(layer);
|
|
||||||
|
|
||||||
if(showInControl) {
|
|
||||||
if(this.layerControl.hasLayer(layer)) {
|
|
||||||
this.layerControl.removeLayer(layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof position !== 'undefined') {
|
|
||||||
this.layerControl.addOverlayAtPosition(layer, name, position);
|
|
||||||
} else {
|
|
||||||
this.layerControl.addOverlay(layer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addHiddenLayer(layer: Layer, name: string, position: number) {
|
|
||||||
if(this.layerControl.hasLayer(layer)) {
|
|
||||||
this.layerControl.removeLayer(layer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof position !== 'undefined') {
|
|
||||||
this.layerControl.addOverlayAtPosition(layer, name, position);
|
|
||||||
} else {
|
|
||||||
this.layerControl.addOverlay(layer, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
removeLayer(layer: Layer) {
|
|
||||||
this.map.removeLayer(layer);
|
|
||||||
this.layerControl.removeLayer(layer);
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "leaflet/controls";
|
|
||||||
@import "leaflet/popups";
|
@import "leaflet/popups";
|
||||||
@import "leaflet/tooltips";
|
@import "leaflet/tooltips";
|
||||||
@import "leaflet/markers";
|
@import "leaflet/markers";
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
padding: 0.8rem 0.8rem 0.7rem;
|
padding: 0.8rem 0.8rem 0.7rem;
|
||||||
line-height: 2rem;
|
line-height: 2rem;
|
||||||
|
letter-spacing: 0.02rem;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
|
@ -53,5 +53,4 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
@ -99,101 +99,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-control-coordinates {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 1.5rem;
|
|
||||||
|
|
||||||
.value {
|
|
||||||
line-height: 1;
|
|
||||||
font-family: monospace;
|
|
||||||
white-space: pre;
|
|
||||||
font-size: 2rem;
|
|
||||||
|
|
||||||
&[data-label]:before {
|
|
||||||
content: attr(data-label);
|
|
||||||
display: block;
|
|
||||||
line-height: 1;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-family: Raleway, sans-serif;;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + .value {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.region {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px), (max-height: 480px) {
|
|
||||||
.value {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 384px) {
|
|
||||||
.chunk {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-layers {
|
|
||||||
width: auto;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-base);
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.leaflet-control-layers-list {
|
|
||||||
@extend %panel;
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: calc(var(--ui-element-spacing) + var(--ui-button-size));
|
|
||||||
overflow: auto;
|
|
||||||
max-width: calc(100vw - 14rem);
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
|
||||||
max-width: calc(100vw - 13rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-layers-overlays {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 30rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layer {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.8rem 0 0.7rem;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: -0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: -0.4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-logo {
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
a {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-top, .leaflet-bottom,
|
.leaflet-top, .leaflet-bottom,
|
||||||
.leaflet-left, .leaflet-right {
|
.leaflet-left, .leaflet-right {
|
||||||
@ -305,17 +210,3 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-control-loading {
|
|
||||||
cursor: wait;
|
|
||||||
animation: fade 0.3s linear;
|
|
||||||
animation-fill-mode: forwards;
|
|
||||||
|
|
||||||
&:hover, &:active, &:focus {
|
|
||||||
background-color: var(--background-base);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -79,7 +79,7 @@ a {
|
|||||||
color: var(--text-base);
|
color: var(--text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
button, [type=button] {
|
button, input[type=button], input[type=reset], input[type=submit] {
|
||||||
@extend %button;
|
@extend %button;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,78 +282,6 @@ img {
|
|||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clock {
|
|
||||||
@extend %panel;
|
|
||||||
position: relative;
|
|
||||||
width: 15rem;
|
|
||||||
height: 6rem;
|
|
||||||
z-index: 50;
|
|
||||||
font-family: monospace;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 2rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.clock__time {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 2rem;
|
|
||||||
line-height: 2rem;
|
|
||||||
margin-top: auto;
|
|
||||||
background-color: var(--background-base);
|
|
||||||
z-index: 1;
|
|
||||||
padding: 0.1rem 0.1rem 0;
|
|
||||||
border-radius: 0.3rem;
|
|
||||||
|
|
||||||
&.night {
|
|
||||||
color: var(--text-night);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.day {
|
|
||||||
color: var(--text-day);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.night, &.day {
|
|
||||||
transition: color 8s 8s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock__sun,
|
|
||||||
.clock__moon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 15rem;
|
|
||||||
height: 12rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.clock--digital {
|
|
||||||
justify-content: center;
|
|
||||||
height: var(--ui-button-size);
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
.clock__sun,
|
|
||||||
.clock__moon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.clock__time {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px), (max-height: 480px) {
|
|
||||||
transform: scale(calc((1/6)*5));
|
|
||||||
transform-origin: top center
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
.form__group {
|
.form__group {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
@ -20,6 +20,7 @@ export enum ActionTypes {
|
|||||||
START_UPDATES = "startUpdates",
|
START_UPDATES = "startUpdates",
|
||||||
STOP_UPDATES = "stopUpdates",
|
STOP_UPDATES = "stopUpdates",
|
||||||
SET_PLAYERS = "setPlayers",
|
SET_PLAYERS = "setPlayers",
|
||||||
|
POP_LAYER_UPDATES = "popLayerUpdates",
|
||||||
POP_MARKER_UPDATES = "popMarkerUpdates",
|
POP_MARKER_UPDATES = "popMarkerUpdates",
|
||||||
POP_TILE_UPDATES = "popTileUpdates",
|
POP_TILE_UPDATES = "popTileUpdates",
|
||||||
SEND_CHAT_MESSAGE = "sendChatMessage",
|
SEND_CHAT_MESSAGE = "sendChatMessage",
|
||||||
|
@ -23,6 +23,7 @@ import {DynmapMarkerUpdate, DynmapTileUpdate} from "@/dynmap";
|
|||||||
import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
|
import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
|
||||||
import {nextTick} from "vue";
|
import {nextTick} from "vue";
|
||||||
import {startUpdateHandling, stopUpdateHandling} from "@/util/markers";
|
import {startUpdateHandling, stopUpdateHandling} from "@/util/markers";
|
||||||
|
import {Layer} from "leaflet";
|
||||||
|
|
||||||
type AugmentedActionContext = {
|
type AugmentedActionContext = {
|
||||||
commit<K extends keyof Mutations>(
|
commit<K extends keyof Mutations>(
|
||||||
@ -49,6 +50,9 @@ export interface Actions {
|
|||||||
{commit}: AugmentedActionContext,
|
{commit}: AugmentedActionContext,
|
||||||
payload: Set<LiveAtlasPlayer>
|
payload: Set<LiveAtlasPlayer>
|
||||||
):Promise<Map<string, LiveAtlasMarkerSet>>
|
):Promise<Map<string, LiveAtlasMarkerSet>>
|
||||||
|
[ActionTypes.POP_LAYER_UPDATES](
|
||||||
|
{commit}: AugmentedActionContext,
|
||||||
|
):Promise<[Layer, boolean][]>
|
||||||
[ActionTypes.POP_MARKER_UPDATES](
|
[ActionTypes.POP_MARKER_UPDATES](
|
||||||
{commit}: AugmentedActionContext,
|
{commit}: AugmentedActionContext,
|
||||||
amount: number
|
amount: number
|
||||||
@ -191,6 +195,14 @@ export const actions: ActionTree<State, State> & Actions = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async [ActionTypes.POP_LAYER_UPDATES]({commit, state}): Promise<[Layer, boolean][]> {
|
||||||
|
const updates = Array.from(state.pendingLayerUpdates.entries());
|
||||||
|
|
||||||
|
commit(MutationTypes.POP_LAYER_UPDATES, undefined);
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
},
|
||||||
|
|
||||||
async [ActionTypes.POP_MARKER_UPDATES]({commit, state}, amount: number): Promise<DynmapMarkerUpdate[]> {
|
async [ActionTypes.POP_MARKER_UPDATES]({commit, state}, amount: number): Promise<DynmapMarkerUpdate[]> {
|
||||||
const updates = state.pendingMarkerUpdates.slice(0, amount);
|
const updates = state.pendingMarkerUpdates.slice(0, amount);
|
||||||
|
|
||||||
|
@ -30,11 +30,15 @@ export enum MutationTypes {
|
|||||||
ADD_MARKER_UPDATES = 'addMarkerUpdates',
|
ADD_MARKER_UPDATES = 'addMarkerUpdates',
|
||||||
ADD_TILE_UPDATES = 'addTileUpdates',
|
ADD_TILE_UPDATES = 'addTileUpdates',
|
||||||
ADD_CHAT = 'addChat',
|
ADD_CHAT = 'addChat',
|
||||||
|
POP_LAYER_UPDATES = 'popLayerUpdates',
|
||||||
POP_MARKER_UPDATES = 'popMarkerUpdates',
|
POP_MARKER_UPDATES = 'popMarkerUpdates',
|
||||||
POP_TILE_UPDATES = 'popTileUpdates',
|
POP_TILE_UPDATES = 'popTileUpdates',
|
||||||
SET_MAX_PLAYERS = 'setMaxPlayers',
|
SET_MAX_PLAYERS = 'setMaxPlayers',
|
||||||
SET_PLAYERS_ASYNC = 'setPlayersAsync',
|
SET_PLAYERS_ASYNC = 'setPlayersAsync',
|
||||||
SYNC_PLAYERS = 'syncPlayers',
|
SYNC_PLAYERS = 'syncPlayers',
|
||||||
|
ADD_LAYER = 'addLayer',
|
||||||
|
UPDATE_LAYER = 'updateLayer',
|
||||||
|
REMOVE_LAYER = 'removeLayer',
|
||||||
SET_LOADED = 'setLoaded',
|
SET_LOADED = 'setLoaded',
|
||||||
|
|
||||||
SET_CURRENT_SERVER = 'setCurrentServer',
|
SET_CURRENT_SERVER = 'setCurrentServer',
|
||||||
|
@ -41,10 +41,12 @@ import {
|
|||||||
LiveAtlasMarker,
|
LiveAtlasMarker,
|
||||||
LiveAtlasMapViewTarget,
|
LiveAtlasMapViewTarget,
|
||||||
LiveAtlasGlobalMessageConfig,
|
LiveAtlasGlobalMessageConfig,
|
||||||
LiveAtlasUIConfig, LiveAtlasServerDefinition
|
LiveAtlasUIConfig, LiveAtlasServerDefinition, LiveAtlasLayerDefinition, LiveAtlasPartialLayerDefinition
|
||||||
} from "@/index";
|
} from "@/index";
|
||||||
import {getServerMapProvider} from "@/util/config";
|
import {getServerMapProvider} from "@/util/config";
|
||||||
import {getDefaultPlayerImage} from "@/util/images";
|
import {getDefaultPlayerImage} from "@/util/images";
|
||||||
|
import {Layer} from "leaflet";
|
||||||
|
import {sortLayers} from "@/util/layers";
|
||||||
|
|
||||||
export type CurrentMapPayload = {
|
export type CurrentMapPayload = {
|
||||||
worldName: string;
|
worldName: string;
|
||||||
@ -67,12 +69,16 @@ export type Mutations<S = State> = {
|
|||||||
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void
|
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void
|
||||||
[MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>): void
|
[MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>): void
|
||||||
|
|
||||||
|
[MutationTypes.POP_LAYER_UPDATES](state: State): void
|
||||||
[MutationTypes.POP_MARKER_UPDATES](state: S, amount: number): void
|
[MutationTypes.POP_MARKER_UPDATES](state: S, amount: number): void
|
||||||
[MutationTypes.POP_TILE_UPDATES](state: S, amount: number): void
|
[MutationTypes.POP_TILE_UPDATES](state: S, amount: number): void
|
||||||
|
|
||||||
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
|
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
|
||||||
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
|
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
|
||||||
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
|
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
|
||||||
|
[MutationTypes.ADD_LAYER](state: State, layer: LiveAtlasLayerDefinition): void
|
||||||
|
[MutationTypes.UPDATE_LAYER](state: State, payload: {layer: Layer, options: LiveAtlasPartialLayerDefinition}): void
|
||||||
|
[MutationTypes.REMOVE_LAYER](state: State, layer: Layer): void
|
||||||
[MutationTypes.SET_LOADED](state: S, a?: void): void
|
[MutationTypes.SET_LOADED](state: S, a?: void): void
|
||||||
[MutationTypes.SET_CURRENT_SERVER](state: S, server: string): void
|
[MutationTypes.SET_CURRENT_SERVER](state: S, server: string): void
|
||||||
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
|
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
|
||||||
@ -173,6 +179,14 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
state.worlds.clear();
|
state.worlds.clear();
|
||||||
state.maps.clear();
|
state.maps.clear();
|
||||||
|
|
||||||
|
//Mark all layers for removal
|
||||||
|
for (const layer of state.layers.keys()) {
|
||||||
|
state.pendingLayerUpdates.set(layer, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.layers.clear();
|
||||||
|
state.sortedLayers.splice(0);
|
||||||
|
|
||||||
state.followTarget = undefined;
|
state.followTarget = undefined;
|
||||||
state.viewTarget = undefined;
|
state.viewTarget = undefined;
|
||||||
|
|
||||||
@ -299,6 +313,11 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
state.chat.messages.unshift(...chat);
|
state.chat.messages.unshift(...chat);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//Pops the specified number of marker updates from the pending updates list
|
||||||
|
[MutationTypes.POP_LAYER_UPDATES](state: State) {
|
||||||
|
state.pendingLayerUpdates.clear();
|
||||||
|
},
|
||||||
|
|
||||||
//Pops the specified number of marker updates from the pending updates list
|
//Pops the specified number of marker updates from the pending updates list
|
||||||
[MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) {
|
[MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) {
|
||||||
state.pendingMarkerUpdates.splice(0, amount);
|
state.pendingMarkerUpdates.splice(0, amount);
|
||||||
@ -379,6 +398,38 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[MutationTypes.ADD_LAYER](state: State, layer: LiveAtlasLayerDefinition) {
|
||||||
|
state.layers.set(layer.layer, layer);
|
||||||
|
state.sortedLayers = sortLayers(state.layers);
|
||||||
|
state.pendingLayerUpdates.set(layer.layer, layer.enabled);
|
||||||
|
},
|
||||||
|
|
||||||
|
[MutationTypes.UPDATE_LAYER](state: State, {layer, options}) {
|
||||||
|
if(state.layers.has(layer)) {
|
||||||
|
const existing = state.layers.get(layer) as LiveAtlasLayerDefinition,
|
||||||
|
existingEnabled = existing.enabled;
|
||||||
|
|
||||||
|
state.layers.set(layer, Object.assign(existing, options));
|
||||||
|
state.sortedLayers = sortLayers(state.layers);
|
||||||
|
|
||||||
|
// Sort layers if position has changed
|
||||||
|
if((typeof options.enabled === 'boolean' && existingEnabled !== options.enabled)) {
|
||||||
|
state.pendingLayerUpdates.set(layer, options.enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
[MutationTypes.REMOVE_LAYER](state: State, layer: Layer) {
|
||||||
|
const existing = state.layers.get(layer);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
console.log('removing???');
|
||||||
|
state.layers.delete(layer);
|
||||||
|
state.pendingLayerUpdates.set(layer, false); // Remove from map
|
||||||
|
state.sortedLayers.splice(state.sortedLayers.indexOf(existing, 1));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
//Sets flag indicating LiveAtlas has fully loaded
|
//Sets flag indicating LiveAtlas has fully loaded
|
||||||
[MutationTypes.SET_LOADED](state: State) {
|
[MutationTypes.SET_LOADED](state: State) {
|
||||||
state.firstLoad = false;
|
state.firstLoad = false;
|
||||||
@ -553,6 +604,15 @@ export const mutations: MutationTree<State> & Mutations = {
|
|||||||
|
|
||||||
state.worlds.clear();
|
state.worlds.clear();
|
||||||
state.maps.clear();
|
state.maps.clear();
|
||||||
|
|
||||||
|
//Mark all layers for removal
|
||||||
|
for (const layer of state.layers.keys()) {
|
||||||
|
state.pendingLayerUpdates.set(layer, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.layers.clear();
|
||||||
|
state.sortedLayers.splice(0);
|
||||||
|
|
||||||
state.currentZoom = 0;
|
state.currentZoom = 0;
|
||||||
state.currentLocation = {x: 0, y: 0, z: 0};
|
state.currentLocation = {x: 0, y: 0, z: 0};
|
||||||
|
|
||||||
|
@ -36,11 +36,12 @@ import {
|
|||||||
LiveAtlasChat,
|
LiveAtlasChat,
|
||||||
LiveAtlasUIModal,
|
LiveAtlasUIModal,
|
||||||
LiveAtlasSidebarSectionState,
|
LiveAtlasSidebarSectionState,
|
||||||
LiveAtlasMarker, LiveAtlasMapViewTarget
|
LiveAtlasMarker, LiveAtlasMapViewTarget, LiveAtlasLayerDefinition
|
||||||
} from "@/index";
|
} from "@/index";
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||||
import {getMessages} from "@/util";
|
import {getMessages} from "@/util";
|
||||||
import {getDefaultPlayerImage} from "@/util/images";
|
import {getDefaultPlayerImage} from "@/util/images";
|
||||||
|
import {Layer} from "leaflet";
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
version: string;
|
version: string;
|
||||||
@ -58,6 +59,10 @@ export type State = {
|
|||||||
|
|
||||||
worlds: Map<string, LiveAtlasWorldDefinition>;
|
worlds: Map<string, LiveAtlasWorldDefinition>;
|
||||||
maps: Map<string, LiveAtlasMapDefinition>;
|
maps: Map<string, LiveAtlasMapDefinition>;
|
||||||
|
|
||||||
|
layers: Map<Layer, LiveAtlasLayerDefinition>;
|
||||||
|
sortedLayers: LiveAtlasLayerDefinition[];
|
||||||
|
|
||||||
players: Map<string, LiveAtlasPlayer>;
|
players: Map<string, LiveAtlasPlayer>;
|
||||||
sortedPlayers: LiveAtlasSortedPlayers;
|
sortedPlayers: LiveAtlasSortedPlayers;
|
||||||
maxPlayers: number;
|
maxPlayers: number;
|
||||||
@ -68,6 +73,7 @@ export type State = {
|
|||||||
messages: LiveAtlasChat[];
|
messages: LiveAtlasChat[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pendingLayerUpdates: Map<Layer, boolean>; //Pending changes to map layer visibility
|
||||||
pendingMarkerUpdates: DynmapMarkerUpdate[];
|
pendingMarkerUpdates: DynmapMarkerUpdate[];
|
||||||
pendingTileUpdates: Array<DynmapTileUpdate>;
|
pendingTileUpdates: Array<DynmapTileUpdate>;
|
||||||
|
|
||||||
@ -130,6 +136,10 @@ export const state: State = {
|
|||||||
|
|
||||||
worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json
|
worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json
|
||||||
maps: new Map(), //Defined maps from configuration.json
|
maps: new Map(), //Defined maps from configuration.json
|
||||||
|
|
||||||
|
layers: new Map(), //Leaflet map layers
|
||||||
|
sortedLayers: [], //Layers sorted by position for layer control
|
||||||
|
|
||||||
players: new Map(), //Online players from world.json
|
players: new Map(), //Online players from world.json
|
||||||
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
|
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
|
||||||
maxPlayers: 0,
|
maxPlayers: 0,
|
||||||
@ -141,6 +151,7 @@ export const state: State = {
|
|||||||
|
|
||||||
markerSets: new Map(), //Marker sets from world_markers.json, doesn't include the markers themselves for performance reasons
|
markerSets: new Map(), //Marker sets from world_markers.json, doesn't include the markers themselves for performance reasons
|
||||||
|
|
||||||
|
pendingLayerUpdates: new Map(), //Pending updates to map layer visibility
|
||||||
pendingMarkerUpdates: [], //Pending updates to markers/areas/etc
|
pendingMarkerUpdates: [], //Pending updates to markers/areas/etc
|
||||||
pendingTileUpdates: [], //Pending updates to map tiles
|
pendingTileUpdates: [], //Pending updates to map tiles
|
||||||
|
|
||||||
|
12
src/util.ts
12
src/util.ts
@ -34,12 +34,22 @@ export const titleColoursRegex = /§[0-9a-f]/ig;
|
|||||||
export const netherWorldNameRegex = /[_\s]?nether([\s_]|$)/i;
|
export const netherWorldNameRegex = /[_\s]?nether([\s_]|$)/i;
|
||||||
export const endWorldNameRegex = /(^|[_\s])end([\s_]|$)/i;
|
export const endWorldNameRegex = /(^|[_\s])end([\s_]|$)/i;
|
||||||
|
|
||||||
|
export interface MinecraftTime {
|
||||||
|
serverTime: number;
|
||||||
|
days: number;
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
seconds: number;
|
||||||
|
day: boolean;
|
||||||
|
night: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates 24 hour time of day and the current day from the given server time
|
* Calculates 24 hour time of day and the current day from the given server time
|
||||||
* @param {number} serverTime Server time in ticks
|
* @param {number} serverTime Server time in ticks
|
||||||
* @returns The equivalent 24 hour time, current day and whether it is currently day or night
|
* @returns The equivalent 24 hour time, current day and whether it is currently day or night
|
||||||
*/
|
*/
|
||||||
export const getMinecraftTime = (serverTime: number) => {
|
export const getMinecraftTime = (serverTime: number): MinecraftTime => {
|
||||||
const day = serverTime >= 0 && serverTime < 13700;
|
const day = serverTime >= 0 && serverTime < 13700;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
44
src/util/layers.ts
Normal file
44
src/util/layers.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 James Lyne
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Layer} from "leaflet";
|
||||||
|
import {LiveAtlasLayerDefinition} from "@/index";
|
||||||
|
import {MutationTypes} from "@/store/mutation-types";
|
||||||
|
import {useStore} from "@/store";
|
||||||
|
|
||||||
|
export const sortLayers = (layers: Map<Layer, LiveAtlasLayerDefinition>) => {
|
||||||
|
return Array.from(layers.values()).sort((entry1, entry2) => {
|
||||||
|
if (entry1.position != entry2.position) {
|
||||||
|
return entry1.position - entry2.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ((entry1.name < entry2.name) ? -1 : ((entry1.name > entry2.name) ? 1 : 0));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toggleLayer = (layer: Layer) => {
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
if(!store.state.layers.has(layer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const enabled = !store.state.layers.get(layer)!.enabled;
|
||||||
|
|
||||||
|
store.commit(MutationTypes.UPDATE_LAYER, {
|
||||||
|
layer: layer,
|
||||||
|
options: {enabled}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user