Compare commits

...

11 Commits

Author SHA1 Message Date
James Lyne
9fe89c2d71 Remove remaining leaflet control references 2022-06-27 23:30:17 +01:00
James Lyne
14674b774e Migrate LayerControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
4887acb917 Migrate ZoomControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
547e64b317 Migrate LogoControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
318ccf6e33 Migrate LoadingControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
7471bb794f Migrate CoordinatesControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
c51d2ef554 Migrate ClockControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
4584410ae2 Migrate LinkControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
627e3219d1 Migrate LoginControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
c6502e5023 Migrate ChatControl to vue 2022-06-27 23:30:17 +01:00
James Lyne
26d5d59b1b Start moving leaflet controls to vue 2022-06-27 23:30:17 +01:00
42 changed files with 1455 additions and 1665 deletions

View File

@ -1,3 +1,3 @@
<component name="DependencyValidationManager">
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&amp;&amp;!file:src/leaflet/control/CoordinatesControl.ts&amp;&amp;!file:src/leaflet/control/LinkControl.ts&amp;&amp;!file:src/leaflet/control/LogoControl.ts&amp;&amp;!file:src/leaflet/icon/PlayerIcon.ts&amp;&amp;!file:src/leaflet/icon/GenericIcon.ts&amp;&amp;!file:src/leaflet/tileLayer/DynmapTileLayer.ts&amp;&amp;!file:src/util/areas.ts&amp;&amp;!file:src/util/circles.ts&amp;&amp;!file:src/util/lines.ts&amp;&amp;!file:src/util/markers.ts&amp;&amp;!file[LiveAtlas]:standalone/*&amp;&amp;!file:src/model/LiveAtlasProjection.ts&amp;&amp;!file:src/leaflet/control/LiveAtlasLayerControl.ts&amp;&amp;!file[LiveAtlas]:patches/*&amp;&amp;!file[LiveAtlas]:public/*&amp;&amp;!file[LiveAtlas]:.idea/*&amp;&amp;!file[LiveAtlas]:.idea//*&amp;&amp;!file[LiveAtlas]:patches//*&amp;&amp;!file[LiveAtlas]:public//*&amp;&amp;!file[LiveAtlas]:standalone//*&amp;&amp;!file:FUNDING.yml&amp;&amp;!file:README.md&amp;&amp;!file:tsconfig.json&amp;&amp;!file:.gitignore&amp;&amp;!file:.env&amp;&amp;!file:LICENSE.md&amp;&amp;!file:package-lock.json&amp;&amp;!file:package.json&amp;&amp;!file:vite.config.ts&amp;&amp;!file:index.html&amp;&amp;!file:src/leaflet/control/LoadingControl.ts&amp;&amp;!file:src/scss/style.scss&amp;&amp;!file[LiveAtlas]:src/assets/icons//*&amp;&amp;!file:src/providers/OverviewerMapProvider.ts&amp;&amp;!file:src/providers/DynmapMapProvider.ts&amp;&amp;!file:src/leaflet/projection/OverviewerProjection.ts&amp;&amp;!file:src/leaflet/tileLayer/OverviewerTileLayer.ts&amp;&amp;!file:jest.config.ts&amp;&amp;!file:.npmignore&amp;&amp;!file:plugin.yml&amp;&amp;!file[LiveAtlas]:java/*&amp;&amp;!file[LiveAtlas]:java//*" />
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&amp;&amp;!file:src/leaflet/control/CoordinatesControl.ts&amp;&amp;!file:src/leaflet/control/LinkControl.ts&amp;&amp;!file:src/leaflet/control/LogoControl.ts&amp;&amp;!file:src/leaflet/icon/PlayerIcon.ts&amp;&amp;!file:src/leaflet/icon/GenericIcon.ts&amp;&amp;!file:src/leaflet/tileLayer/DynmapTileLayer.ts&amp;&amp;!file:src/util/areas.ts&amp;&amp;!file:src/util/circles.ts&amp;&amp;!file:src/util/lines.ts&amp;&amp;!file:src/util/markers.ts&amp;&amp;!file[LiveAtlas]:standalone/*&amp;&amp;!file:src/model/LiveAtlasProjection.ts&amp;&amp;!file:src/leaflet/control/LiveAtlasLayerControl.ts&amp;&amp;!file[LiveAtlas]:patches/*&amp;&amp;!file[LiveAtlas]:public/*&amp;&amp;!file[LiveAtlas]:.idea/*&amp;&amp;!file[LiveAtlas]:.idea//*&amp;&amp;!file[LiveAtlas]:patches//*&amp;&amp;!file[LiveAtlas]:public//*&amp;&amp;!file[LiveAtlas]:standalone//*&amp;&amp;!file:FUNDING.yml&amp;&amp;!file:README.md&amp;&amp;!file:tsconfig.json&amp;&amp;!file:.gitignore&amp;&amp;!file:.env&amp;&amp;!file:LICENSE.md&amp;&amp;!file:package-lock.json&amp;&amp;!file:package.json&amp;&amp;!file:vite.config.ts&amp;&amp;!file:index.html&amp;&amp;!file:src/leaflet/control/LoadingControl.ts&amp;&amp;!file:src/scss/style.scss&amp;&amp;!file[LiveAtlas]:src/assets/icons//*&amp;&amp;!file:src/providers/OverviewerMapProvider.ts&amp;&amp;!file:src/providers/DynmapMapProvider.ts&amp;&amp;!file:src/leaflet/projection/OverviewerProjection.ts&amp;&amp;!file:src/leaflet/tileLayer/OverviewerTileLayer.ts&amp;&amp;!file:jest.config.ts&amp;&amp;!file:.npmignore&amp;&amp;!file:plugin.yml&amp;&amp;!file[LiveAtlas]:java/*&amp;&amp;!file[LiveAtlas]:java//*&amp;&amp;!file:src/components/map/control/LoadingControl.vue" />
</component>

12
package-lock.json generated
View File

@ -11,7 +11,7 @@
"dependencies": {
"@kyvg/vue3-notification": "2.3.0",
"@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",
"modern-normalize": "^1.1.0",
"vue": "^3.2.37",
@ -7606,8 +7606,8 @@
},
"node_modules/leaflet": {
"version": "1.8.0-LiveAtlas",
"resolved": "git+ssh://git@github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
"integrity": "sha512-QJPpfKcc+xgcOzB6KlJEjdw4RJ41rMSeeTqPD7QsO1CvJ9uiJ/yhW4iSTKyhsmZ3JJDBw8Do2+cLevkddyLR9w==",
"resolved": "git+ssh://git@github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
"integrity": "sha512-Fptn2BrpixOsx/+dAzObvWcdZML6+rCjSGxB8oeXt4sBCxHj4QuFB/8bAxMgzQW72oE0SytmMmLN+38WTRq3gA==",
"license": "BSD-2-Clause"
},
"node_modules/leven": {
@ -16196,9 +16196,9 @@
"peer": true
},
"leaflet": {
"version": "git+ssh://git@github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1",
"integrity": "sha512-QJPpfKcc+xgcOzB6KlJEjdw4RJ41rMSeeTqPD7QsO1CvJ9uiJ/yhW4iSTKyhsmZ3JJDBw8Do2+cLevkddyLR9w==",
"from": "leaflet@git+https://github.com/JLyne/Leaflet.git#843eb3124492dc48245cd187c6dc94c2f33b65c1"
"version": "git+ssh://git@github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376",
"integrity": "sha512-Fptn2BrpixOsx/+dAzObvWcdZML6+rCjSGxB8oeXt4sBCxHj4QuFB/8bAxMgzQW72oE0SytmMmLN+38WTRq3gA==",
"from": "leaflet@git+https://github.com/JLyne/Leaflet.git#0bf4e3f70c2559771592c077401027a6c4913376"
},
"leven": {
"version": "3.1.0",

View File

@ -18,7 +18,7 @@
"dependencies": {
"@kyvg/vue3-notification": "2.3.0",
"@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",
"modern-normalize": "^1.1.0",
"vue": "^3.2.37",

View File

@ -15,8 +15,9 @@
-->
<template>
<Map></Map>
<ChatBox v-if="chatBoxEnabled" v-show="chatBoxEnabled && chatVisible"></ChatBox>
<Map v-slot="slotProps">
<MapUI v-if="slotProps.leaflet" :leaflet="slotProps.leaflet"></MapUI>
</Map>
<LoginModal v-if="loginEnabled" v-show="loginModalVisible" :required="loginRequired"></LoginModal>
<Sidebar></Sidebar>
<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 Map from './components/Map.vue';
import Sidebar from './components/Sidebar.vue';
import ChatBox from './components/ChatBox.vue';
import {useStore} from "@/store";
import {ActionTypes} from "@/store/action-types";
import {parseUrl} from '@/util';
@ -36,13 +36,14 @@ import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
import LoginModal from "@/components/login/LoginModal.vue";
import {notify} from "@kyvg/vue3-notification";
import {clearPlayerImageCache} from "@/util/images";
import MapUI from "@/components/MapUI.vue";
export default defineComponent({
name: 'App',
components: {
MapUI,
Map,
Sidebar,
ChatBox,
LoginModal
},
@ -54,8 +55,6 @@ export default defineComponent({
currentUrl = computed(() => store.getters.url),
currentServer = computed(() => store.state.currentServer),
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),
loggedIn = computed(() => store.state.loggedIn), //Whether the user is currently logged in
@ -225,8 +224,6 @@ export default defineComponent({
});
return {
chatBoxEnabled,
chatVisible,
loginEnabled,
loginRequired,
loginModalVisible

View File

@ -15,18 +15,18 @@
-->
<template>
<section class="chat">
<ul class="chat__messages" role="log" aria-live="polite" aria-relevant="additions">
<section class="chatbox">
<ul class="chatbox__messages" role="log" aria-live="polite" aria-relevant="additions">
<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>
</ul>
<form v-if="sendingEnabled" class="chat__form" @submit.prevent="sendMessage">
<div role="alert" v-if="sendingError" class="chat__error">{{ sendingError }}</div>
<input ref="chatInput" v-model="enteredMessage" class="chat__input" type="text" :maxlength="maxMessageLength"
<form v-if="sendingEnabled" class="chatbox__form" @submit.prevent="sendMessage">
<div role="alert" v-if="sendingError" class="chatbox__error">{{ sendingError }}</div>
<input ref="chatInput" v-model="enteredMessage" class="chatbox__input" type="text" :maxlength="maxMessageLength"
: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>
<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>
</template>
@ -129,18 +129,12 @@
<style lang="scss">
@import '../scss/placeholders';
.chat {
.chatbox {
@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;
box-sizing: border-box;
.chat__messages {
.chatbox__messages {
display: flex;
flex-direction: column-reverse;
list-style: none;
@ -163,24 +157,24 @@
}
}
.chat__form {
.chatbox__form {
display: flex;
flex-wrap: wrap;
align-items: stretch;
margin: 1.5rem -1.5rem -1.5rem;
.chat__input {
.chatbox__input {
border-bottom-left-radius: var(--border-radius);
flex-grow: 1;
}
.chat__send {
.chatbox__send {
padding-left: 1rem;
padding-right: 1rem;
border-radius: 0 0 var(--border-radius) 0;
}
.chat__error {
.chatbox__error {
background-color: var(--background-error);
color: var(--text-emphasis);
font-size: 1.6rem;
@ -190,7 +184,7 @@
}
}
.chat__login {
.chatbox__login {
font-size: 1.6rem;
padding: 1.2rem;
background-color: var(--background-light);
@ -206,7 +200,7 @@
}
@media (max-width: 320px) {
.chat__messages .message + .message {
.chatbox__messages .message + .message {
margin-bottom: 0.7rem;
}
}

View File

@ -22,18 +22,10 @@
<TileLayerOverlay v-for="[name, overlay] in overlays" :key="name" :options="overlay" :leaflet="leaflet"></TileLayerOverlay>
<PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
<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>
</div>
<MapContextMenu v-if="contextMenuEnabled && leaflet" :leaflet="leaflet"></MapContextMenu>
<slot :leaflet="leaflet"></slot>
</template>
<script lang="ts">
@ -43,32 +35,18 @@ import {useStore} from '@/store';
import TileLayer from "@/components/map/layer/TileLayer.vue";
import PlayersLayer from "@/components/map/layer/PlayersLayer.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 LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LoadingControl} from "@/leaflet/control/LoadingControl";
import MapContextMenu from "@/components/map/MapContextMenu.vue";
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
import LoginControl from "@/components/map/control/LoginControl.vue";
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
import {ActionTypes} from "@/store/action-types";
export default defineComponent({
components: {
TileLayerOverlay,
MapContextMenu,
TileLayer,
PlayersLayer,
MarkerSetLayer,
CoordinatesControl,
ClockControl,
LinkControl,
ChatControl,
LogoControl,
LoginControl
MarkerSetLayer
},
setup() {
@ -81,13 +59,6 @@ export default defineComponent({
configuration = computed(() => store.state.configuration),
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),
currentMap = computed(() => store.state.currentMap),
@ -99,6 +70,7 @@ export default defineComponent({
//Location and zoom to pan to upon next projection change
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
pendingLayerUpdates = computed(() => !!store.state.pendingLayerUpdates.size),
mapTitle = computed(() => store.state.messages.mapTitle);
@ -110,14 +82,7 @@ export default defineComponent({
configuration,
playerMarkersEnabled,
coordinatesControlEnabled,
clockControlEnabled,
linkControlEnabled,
chatBoxEnabled,
loginEnabled,
contextMenuEnabled,
logoControls,
followTarget,
viewTarget,
parsedUrl,
@ -127,6 +92,7 @@ export default defineComponent({
currentMap,
scheduledView,
pendingLayerUpdates,
mapTitle
}
@ -227,6 +193,21 @@ export default defineComponent({
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: {
handler(newValue) {
if(!newValue || !this.currentMap || !this.leaflet) {
@ -253,9 +234,7 @@ export default defineComponent({
center: new LatLng(0, 0),
fadeAnimation: false,
zoomAnimation: true,
zoomControl: true,
preferCanvas: true,
attributionControl: false,
crs: CRS.Simple,
worldCopyJump: false,
// markerZoomAnimation: false,
@ -265,11 +244,6 @@ export default defineComponent({
this.leaflet.createPane('vectors');
this.leaflet.addControl(new LoadingControl({
position: 'topleft',
delayIndicator: 500,
}));
this.leaflet.on('moveend', () => {
if(this.currentMap) {
useStore().commit(MutationTypes.SET_CURRENT_LOCATION, this.currentMap

218
src/components/MapUI.vue Normal file
View 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>

View File

@ -14,26 +14,48 @@
- 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">
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import {ChatControl} from "@/leaflet/control/ChatControl";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {computed, defineComponent} from "@vue/runtime-core";
import {useStore} from "@/store";
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({
props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
}
components: {
ChatBox,
SvgIcon,
},
setup(props) {
const control = new ChatControl({
position: 'topleft',
});
setup() {
const store = useStore(),
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
buttonTitle = computed(() => store.state.messages.chatTitle);
onMounted(() => props.leaflet.addControl(control));
onUnmounted(() => props.leaflet.removeControl(control));
const handleClick = () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'chat'),
handleKeydown = () =>
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'chat', state: true});
return {
buttonTitle,
chatVisible,
handleClick,
handleKeydown
}
},
render() {
@ -41,3 +63,19 @@ export default defineComponent({
}
})
</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>

View File

@ -14,43 +14,205 @@
- 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">
import {computed, defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import {computed, defineComponent} from "@vue/runtime-core";
import {useStore} from "@/store";
import {ClockControl, ClockControlOptions} from "@/leaflet/control/ClockControl";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {watch} from "vue";
import SvgIcon from "@/components/SvgIcon.vue";
import {getMinecraftTime} from "@/util";
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({
props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
}
},
setup(props) {
components: {SvgIcon},
setup() {
const store = useStore(),
componentSettings = computed(() => store.state.components.clockControl);
let control = new ClockControl(componentSettings.value as ClockControlOptions) as ClockControl;
componentSettings = computed(() => store.state.components.clockControl),
digital = computed(() => componentSettings.value!.showTimeOfDay && !componentSettings.value!.showWeather && componentSettings.value!.showDigitalClock),
showTime = computed(() => componentSettings.value!.showDigitalClock),
watch(componentSettings, (newSettings) => {
props.leaflet.removeControl(control);
worldState = computed(() => store.state.currentWorldState),
minecraftTime = computed(() => getMinecraftTime(worldState.value.timeOfDay)),
if(!newSettings) {
return;
formattedTime = computed(() => {
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);
props.leaflet.addControl(control);
}, {deep: true});
return 'clock_sun';
}),
onMounted(() => props.leaflet.addControl(control));
onUnmounted(() => props.leaflet.removeControl(control));
},
moonIcon = computed(() => {
if (componentSettings.value!.showWeather) {
if (worldState.value.thundering) {
return 'clock_moon_storm';
} else if (worldState.value.raining) {
return 'clock_moon_rain';
}
}
render() {
return null;
return 'clock_moon';
});
return {
digital,
showTime,
minecraftTime,
formattedTime,
sunStyle,
moonStyle,
sunIcon,
moonIcon
}
}
})
</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>

View File

@ -14,12 +14,24 @@
- 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">
import {computed, defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import {CoordinatesControl, CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
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({
props: {
@ -31,26 +43,129 @@ export default defineComponent({
setup(props) {
const store = useStore(),
componentSettings = computed(() => store.state.components.coordinatesControl);
let control = new CoordinatesControl(componentSettings.value as CoordinatesControlOptions);
componentSettings = computed(() => store.state.components.coordinatesControl as CoordinatesControlOptions),
currentMap = computed(() => store.state.currentMap),
watch(componentSettings, (newSettings) => {
props.leaflet.removeControl(control);
chunkLabel = computed(() => store.state.messages.locationChunk),
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;
}
control = new CoordinatesControl(newSettings as CoordinatesControlOptions);
props.leaflet.addControl(control);
}, {deep: true});
coordinates.value = store.state.currentMap.latLngToLocation(event.latlng, store.state.currentWorld!.seaLevel + 1);
}
onMounted(() => props.leaflet.addControl(control));
onUnmounted(() => props.leaflet.removeControl(control));
},
const onMouseOut = () => coordinates.value = null;
render() {
return null;
watch(currentMap, newValue => {
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>
<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>

View 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>

View File

@ -13,31 +13,34 @@
- See the License for the specific language governing permissions and
- 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">
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import {LinkControl} from "@/leaflet/control/LinkControl";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {computed, defineComponent} from "@vue/runtime-core";
import SvgIcon from "@/components/SvgIcon.vue";
import {useStore} from "@/store";
import {clipboardError, clipboardSuccess} from "@/util";
import '@/assets/icons/link.svg';
export default defineComponent({
props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
components: {SvgIcon},
setup() {
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>

View 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>

View File

@ -14,30 +14,56 @@
- 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">
import {defineComponent, onMounted, onUnmounted} from "@vue/runtime-core";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LoginControl} from "@/leaflet/control/LoginControl";
import {computed, defineComponent} from "@vue/runtime-core";
import SvgIcon from "@/components/SvgIcon.vue";
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({
props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
components: {SvgIcon},
setup() {
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>

View File

@ -14,45 +14,47 @@
- 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">
import {defineComponent} from "@vue/runtime-core";
import {LogoControl, LogoControlOptions} from "@/leaflet/control/LogoControl";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LogoControlOptions} from "@/index";
export default defineComponent({
props: {
options: {
type: Object as () => LogoControlOptions,
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>
<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>

View 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>

View File

@ -21,11 +21,11 @@
<script lang="ts">
import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core";
import {useStore} from "@/store";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {LiveAtlasMarkerSet} from "@/index";
import {watch} from "vue";
import {markRaw, watch} from "vue";
import Markers from "@/components/map/marker/Markers.vue";
import {MutationTypes} from "@/store/mutation-types";
export default defineComponent({
components: {
@ -33,11 +33,6 @@ export default defineComponent({
},
props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
},
markerSet: {
type: Object as () => LiveAtlasMarkerSet,
required: true,
@ -65,27 +60,25 @@ export default defineComponent({
priority: props.markerSet.priority,
});
if(newValue.hidden) {
props.leaflet.getLayerManager()
.addHiddenLayer(layerGroup, newValue.label, props.markerSet.priority);
} else {
props.leaflet.getLayerManager()
.addLayer(layerGroup, true, newValue.label, props.markerSet.priority);
}
// store.commit(MutationTypes.UPDATE_LAYER, {
// layer: layerGroup,
// options: {enabled: newValue.hidden}
// });
}
}, {deep: true});
onMounted(() => {
if(props.markerSet.hidden) {
props.leaflet.getLayerManager()
.addHiddenLayer(layerGroup, props.markerSet.label, props.markerSet.priority);
} else {
props.leaflet.getLayerManager()
.addLayer(layerGroup, true, props.markerSet.label, props.markerSet.priority);
}
store.commit(MutationTypes.ADD_LAYER, {
layer: markRaw(layerGroup),
name: props.markerSet.label,
overlay: true,
position: props.markerSet.priority || 0,
enabled: !props.markerSet.hidden,
showInControl: true
});
});
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
return {
markerSettings,

View File

@ -24,6 +24,8 @@ import {defineComponent, computed, watch, onMounted, onUnmounted} from "@vue/run
import {useStore} from "@/store";
import {LayerGroup} from 'leaflet';
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {MutationTypes} from "@/store/mutation-types";
import {markRaw} from "vue";
export default defineComponent({
components: {
@ -51,21 +53,17 @@ export default defineComponent({
watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150));
onMounted(() => {
if(!componentSettings.value!.hideByDefault) {
props.leaflet.getLayerManager().addLayer(
layerGroup,
true,
store.state.components.players.markers!.layerName,
componentSettings.value!.layerPriority);
} else {
props.leaflet.getLayerManager().addHiddenLayer(
layerGroup,
store.state.components.players.markers!.layerName,
componentSettings.value!.layerPriority);
}
store.commit(MutationTypes.ADD_LAYER, {
layer: markRaw(layerGroup),
name: store.state.components.players.markers!.layerName,
overlay: true,
position: componentSettings.value!.layerPriority,
enabled: !componentSettings.value!.hideByDefault,
showInControl: true
});
});
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
if(playersAboveMarkers.value) {
playerPane.style.zIndex = '600';

View File

@ -19,6 +19,8 @@ import {defineComponent, onUnmounted} from "@vue/runtime-core";
import {useStore} from "@/store";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LiveAtlasTileLayerOverlay} from "@/index";
import {MutationTypes} from "@/store/mutation-types";
import {markRaw} from "vue";
export default defineComponent({
props: {
@ -37,10 +39,17 @@ export default defineComponent({
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(() => {
props.leaflet.getLayerManager().removeLayer(layer);
store.commit(MutationTypes.REMOVE_LAYER, layer)
layer.remove();
});
},

42
src/index.d.ts vendored
View File

@ -18,16 +18,14 @@ import {State} from "@/store";
import {DynmapUrlConfig} from "@/dynmap";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {
ControlOptions,
Coords,
DoneCallback, FitBoundsOptions,
InternalTiles, LatLng,
InternalTiles, LatLng, Layer,
PathOptions,
PointTuple,
PolylineOptions
} 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 {LiveAtlasMarkerType} from "@/util/markers";
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
@ -173,6 +171,23 @@ interface LiveAtlasWorldState {
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 {
world?: string;
map?: string;
@ -291,6 +306,25 @@ interface LiveAtlasComponentConfig {
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 {
markers?: {
showLabels: boolean;

View File

@ -14,44 +14,14 @@
* limitations under the License.
*/
import {Map, DomUtil, MapOptions} from 'leaflet';
import LayerManager from "@/leaflet/layer/LayerManager";
import {Map, MapOptions} from 'leaflet';
export default class LiveAtlasLeafletMap extends Map {
declare _controlCorners: any;
declare _controlContainer?: HTMLElement;
declare _container?: HTMLElement;
private readonly _layerManager: LayerManager;
constructor(element: string | HTMLElement, options?: MapOptions) {
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');
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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}`;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
@import "leaflet/controls";
@import "leaflet/popups";
@import "leaflet/tooltips";
@import "leaflet/markers";

View File

@ -40,6 +40,7 @@
user-select: none;
padding: 0.8rem 0.8rem 0.7rem;
line-height: 2rem;
letter-spacing: 0.02rem;
-webkit-tap-highlight-color: transparent;
.svg-icon {

View File

@ -53,5 +53,4 @@
display: flex;
flex-direction: column;
padding: 1.5rem;
position: relative;
}

View File

@ -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-left, .leaflet-right {
@ -305,17 +210,3 @@
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;
}
}

View File

@ -79,7 +79,7 @@ a {
color: var(--text-base);
}
button, [type=button] {
button, input[type=button], input[type=reset], input[type=submit] {
@extend %button;
}
@ -282,78 +282,6 @@ img {
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__group {
margin-bottom: 1.5rem;

View File

@ -20,6 +20,7 @@ export enum ActionTypes {
START_UPDATES = "startUpdates",
STOP_UPDATES = "stopUpdates",
SET_PLAYERS = "setPlayers",
POP_LAYER_UPDATES = "popLayerUpdates",
POP_MARKER_UPDATES = "popMarkerUpdates",
POP_TILE_UPDATES = "popTileUpdates",
SEND_CHAT_MESSAGE = "sendChatMessage",

View File

@ -23,6 +23,7 @@ import {DynmapMarkerUpdate, DynmapTileUpdate} from "@/dynmap";
import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
import {nextTick} from "vue";
import {startUpdateHandling, stopUpdateHandling} from "@/util/markers";
import {Layer} from "leaflet";
type AugmentedActionContext = {
commit<K extends keyof Mutations>(
@ -49,6 +50,9 @@ export interface Actions {
{commit}: AugmentedActionContext,
payload: Set<LiveAtlasPlayer>
):Promise<Map<string, LiveAtlasMarkerSet>>
[ActionTypes.POP_LAYER_UPDATES](
{commit}: AugmentedActionContext,
):Promise<[Layer, boolean][]>
[ActionTypes.POP_MARKER_UPDATES](
{commit}: AugmentedActionContext,
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[]> {
const updates = state.pendingMarkerUpdates.slice(0, amount);

View File

@ -30,11 +30,15 @@ export enum MutationTypes {
ADD_MARKER_UPDATES = 'addMarkerUpdates',
ADD_TILE_UPDATES = 'addTileUpdates',
ADD_CHAT = 'addChat',
POP_LAYER_UPDATES = 'popLayerUpdates',
POP_MARKER_UPDATES = 'popMarkerUpdates',
POP_TILE_UPDATES = 'popTileUpdates',
SET_MAX_PLAYERS = 'setMaxPlayers',
SET_PLAYERS_ASYNC = 'setPlayersAsync',
SYNC_PLAYERS = 'syncPlayers',
ADD_LAYER = 'addLayer',
UPDATE_LAYER = 'updateLayer',
REMOVE_LAYER = 'removeLayer',
SET_LOADED = 'setLoaded',
SET_CURRENT_SERVER = 'setCurrentServer',

View File

@ -41,10 +41,12 @@ import {
LiveAtlasMarker,
LiveAtlasMapViewTarget,
LiveAtlasGlobalMessageConfig,
LiveAtlasUIConfig, LiveAtlasServerDefinition
LiveAtlasUIConfig, LiveAtlasServerDefinition, LiveAtlasLayerDefinition, LiveAtlasPartialLayerDefinition
} from "@/index";
import {getServerMapProvider} from "@/util/config";
import {getDefaultPlayerImage} from "@/util/images";
import {Layer} from "leaflet";
import {sortLayers} from "@/util/layers";
export type CurrentMapPayload = {
worldName: string;
@ -67,12 +69,16 @@ export type Mutations<S = State> = {
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): 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_TILE_UPDATES](state: S, amount: number): void
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
[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_CURRENT_SERVER](state: S, server: string): void
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
@ -173,6 +179,14 @@ export const mutations: MutationTree<State> & Mutations = {
state.worlds.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.viewTarget = undefined;
@ -299,6 +313,11 @@ export const mutations: MutationTree<State> & Mutations = {
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
[MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) {
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
[MutationTypes.SET_LOADED](state: State) {
state.firstLoad = false;
@ -553,6 +604,15 @@ export const mutations: MutationTree<State> & Mutations = {
state.worlds.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.currentLocation = {x: 0, y: 0, z: 0};

View File

@ -36,11 +36,12 @@ import {
LiveAtlasChat,
LiveAtlasUIModal,
LiveAtlasSidebarSectionState,
LiveAtlasMarker, LiveAtlasMapViewTarget
LiveAtlasMarker, LiveAtlasMapViewTarget, LiveAtlasLayerDefinition
} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {getMessages} from "@/util";
import {getDefaultPlayerImage} from "@/util/images";
import {Layer} from "leaflet";
export type State = {
version: string;
@ -58,6 +59,10 @@ export type State = {
worlds: Map<string, LiveAtlasWorldDefinition>;
maps: Map<string, LiveAtlasMapDefinition>;
layers: Map<Layer, LiveAtlasLayerDefinition>;
sortedLayers: LiveAtlasLayerDefinition[];
players: Map<string, LiveAtlasPlayer>;
sortedPlayers: LiveAtlasSortedPlayers;
maxPlayers: number;
@ -68,6 +73,7 @@ export type State = {
messages: LiveAtlasChat[];
};
pendingLayerUpdates: Map<Layer, boolean>; //Pending changes to map layer visibility
pendingMarkerUpdates: DynmapMarkerUpdate[];
pendingTileUpdates: Array<DynmapTileUpdate>;
@ -130,6 +136,10 @@ export const state: State = {
worlds: new Map(), //Defined (loaded) worlds with 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
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
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
pendingLayerUpdates: new Map(), //Pending updates to map layer visibility
pendingMarkerUpdates: [], //Pending updates to markers/areas/etc
pendingTileUpdates: [], //Pending updates to map tiles

View File

@ -34,12 +34,22 @@ export const titleColoursRegex = /§[0-9a-f]/ig;
export const netherWorldNameRegex = /[_\s]?nether([\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
* @param {number} serverTime Server time in ticks
* @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;
return {

44
src/util/layers.ts Normal file
View 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}
});
}