切换echarts 问题1 原先的打标点产生冲突 -- 解决中

main
王宏建 2023-09-22 15:48:40 +08:00
parent ca28c0851e
commit b0c004eab8
38 changed files with 40987 additions and 545 deletions

View File

@ -13,13 +13,17 @@
"@types/leaflet-draw": "^1.0.7", "@types/leaflet-draw": "^1.0.7",
"@types/proj4leaflet": "^1.0.7", "@types/proj4leaflet": "^1.0.7",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.2.1",
"echarts": "^5.4.3",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"heatmapjs": "^2.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet-draw": "^1.0.4", "leaflet-draw": "^1.0.4",
"pinia": "^2.1.6",
"proj4": "^2.9.0", "proj4": "^2.9.0",
"proj4leaflet": "^1.0.2", "proj4leaflet": "^1.0.2",
"quasar": "^2.12.3", "quasar": "^2.12.3",
"vue": "^3.3.4" "vue": "^3.3.4",
"vue-router": "4"
}, },
"devDependencies": { "devDependencies": {
"@quasar/vite-plugin": "^1.4.1", "@quasar/vite-plugin": "^1.4.1",

View File

@ -17,15 +17,24 @@ dependencies:
'@vueuse/core': '@vueuse/core':
specifier: ^10.2.1 specifier: ^10.2.1
version: 10.2.1(vue@3.3.4) version: 10.2.1(vue@3.3.4)
echarts:
specifier: ^5.4.3
version: 5.4.3
geojson: geojson:
specifier: ^0.5.0 specifier: ^0.5.0
version: 0.5.0 version: 0.5.0
heatmapjs:
specifier: ^2.0.2
version: 2.0.2
leaflet: leaflet:
specifier: ^1.9.4 specifier: ^1.9.4
version: 1.9.4 version: 1.9.4
leaflet-draw: leaflet-draw:
specifier: ^1.0.4 specifier: ^1.0.4
version: 1.0.4 version: 1.0.4
pinia:
specifier: ^2.1.6
version: 2.1.6(typescript@5.1.6)(vue@3.3.4)
proj4: proj4:
specifier: ^2.9.0 specifier: ^2.9.0
version: 2.9.0 version: 2.9.0
@ -38,6 +47,9 @@ dependencies:
vue: vue:
specifier: ^3.3.4 specifier: ^3.3.4
version: 3.3.4 version: 3.3.4
vue-router:
specifier: '4'
version: 4.2.4(vue@3.3.4)
devDependencies: devDependencies:
'@quasar/vite-plugin': '@quasar/vite-plugin':
@ -405,6 +417,10 @@ packages:
'@vue/compiler-dom': 3.3.4 '@vue/compiler-dom': 3.3.4
'@vue/shared': 3.3.4 '@vue/shared': 3.3.4
/@vue/devtools-api@6.5.0:
resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==}
dev: false
/@vue/language-core@1.8.5(typescript@5.1.6): /@vue/language-core@1.8.5(typescript@5.1.6):
resolution: {integrity: sha512-DKQNiNQzNV7nrkZQujvjfX73zqKdj2+KoM4YeKl+ft3f+crO3JB4ycPnmgaRMNX/ULJootdQPGHKFRl5cXxwaw==} resolution: {integrity: sha512-DKQNiNQzNV7nrkZQujvjfX73zqKdj2+KoM4YeKl+ft3f+crO3JB4ycPnmgaRMNX/ULJootdQPGHKFRl5cXxwaw==}
peerDependencies: peerDependencies:
@ -549,6 +565,13 @@ packages:
resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==}
dev: true dev: true
/echarts@5.4.3:
resolution: {integrity: sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==}
dependencies:
tslib: 2.3.0
zrender: 5.4.4
dev: false
/esbuild@0.18.13: /esbuild@0.18.13:
resolution: {integrity: sha512-vhg/WR/Oiu4oUIkVhmfcc23G6/zWuEQKFS+yiosSHe4aN6+DQRXIfeloYGibIfVhkr4wyfuVsGNLr+sQU1rWWw==} resolution: {integrity: sha512-vhg/WR/Oiu4oUIkVhmfcc23G6/zWuEQKFS+yiosSHe4aN6+DQRXIfeloYGibIfVhkr4wyfuVsGNLr+sQU1rWWw==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -614,6 +637,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/heatmapjs@2.0.2:
resolution: {integrity: sha512-1pO/bbn9G1NYhndvjnzLVAQMGBOCk8abMM7QnLOPlIMoxDfG9Uylb68PlZe/0MCT9GuwGGtchXVPLWfoT6HrCw==}
dev: false
/is-binary-path@2.1.0: /is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -692,6 +719,24 @@ packages:
engines: {node: '>=8.6'} engines: {node: '>=8.6'}
dev: true dev: true
/pinia@2.1.6(typescript@5.1.6)(vue@3.3.4):
resolution: {integrity: sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==}
peerDependencies:
'@vue/composition-api': ^1.4.0
typescript: '>=4.4.4'
vue: ^2.6.14 || ^3.3.0
peerDependenciesMeta:
'@vue/composition-api':
optional: true
typescript:
optional: true
dependencies:
'@vue/devtools-api': 6.5.0
typescript: 5.1.6
vue: 3.3.4
vue-demi: 0.14.5(vue@3.3.4)
dev: false
/postcss@8.4.26: /postcss@8.4.26:
resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==} resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
@ -763,11 +808,14 @@ packages:
is-number: 7.0.0 is-number: 7.0.0
dev: true dev: true
/tslib@2.3.0:
resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==}
dev: false
/typescript@5.1.6: /typescript@5.1.6:
resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
dev: true
/vite@4.4.4(sass@1.32.12): /vite@4.4.4(sass@1.32.12):
resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==} resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==}
@ -820,6 +868,15 @@ packages:
vue: 3.3.4 vue: 3.3.4
dev: false dev: false
/vue-router@4.2.4(vue@3.3.4):
resolution: {integrity: sha512-9PISkmaCO02OzPVOMq2w82ilty6+xJmQrarYZDkjZBfl4RvYAlt4PKnEX21oW4KTtWfa9OuO/b3qk1Od3AEdCQ==}
peerDependencies:
vue: ^3.2.0
dependencies:
'@vue/devtools-api': 6.5.0
vue: 3.3.4
dev: false
/vue-template-compiler@2.7.14: /vue-template-compiler@2.7.14:
resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==} resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
dependencies: dependencies:
@ -855,3 +912,9 @@ packages:
/yallist@4.0.0: /yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true dev: true
/zrender@5.4.4:
resolution: {integrity: sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==}
dependencies:
tslib: 2.3.0
dev: false

View File

@ -1,129 +1,115 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeMount, ref, watch } from 'vue'; import { onBeforeMount, ref, watch } from 'vue';
import Map from './components/Map.vue' import ThemeSwitch from './components/ThemeSwitch.vue';
import Mark from './components/mapChild/Mark.vue' import MarkMenu from './components/left/MarkMenu.vue';
import useMarks, { markType } from './hook/useMarks.ts' import CtrlMenu from './components/left/CtrlMenu.vue';
import ThemeSwitch from './components/ThemeSwitch.vue' import { useWindowSize } from '@vueuse/core';
import MarkMenu from './components/left/MarkMenu.vue' import { useRoute, useRouter } from 'vue-router';
import CtrlMenu from './components/left/CtrlMenu.vue' import { useMarkStore } from './store/useMarkStore';
import { mdiLanConnect } from '@quasar/extras/mdi-v7'
import { useWindowSize } from '@vueuse/core';
import { Config } from './hook/useConfig';
const {marks} = useMarkStore();
const tab = ref('mark');
const sideStyle = ref('20em,logo');
const { width } = useWindowSize();
const route = useRoute();
const router = useRouter();
const nowRouter = ref(route.path);
const map = ref<InstanceType<typeof Map>>(); // watch(nowRouter, (path) => router.push(path));
const marks = useMarks([{ latlng: [39.92647400, 116.40328300], title: '北京故宫', opacity: 1 }])
const tab = ref('mark')
const sideStyle = ref('20em,logo');
const { width } = useWindowSize()
const mapConfig = ref()
onBeforeMount(() => { onBeforeMount(() => {
sideStyle.value = width.value > 600 ? '20em,logo' : '0em,bar' sideStyle.value = width.value > 600 ? '20em,logo' : '0em,bar';
}) });
function changeSide(data: string) { function changeSide(data: string) {
const _data = data.split(',') const _data = data.split(',');
document.body.style.setProperty('--side', _data[0]) document.body.style.setProperty('--side', _data[0]);
document.body.style.setProperty('--logoOrBar', _data[1]) document.body.style.setProperty('--logoOrBar', _data[1]);
} }
watch(sideStyle, changeSide)
function pushMark(params: any, type: markType) { watch(sideStyle, changeSide);
const { latlng } = params;
marks.value.push({ latlng: [latlng.lat, latlng.lng], title: '新标点', opacity: 1, type })
}
function removeMark(index: number) {
marks.value.splice(index, 1)
}
function flytoMark(index: number) {
mapConfig.value.center = marks.value[index].latlng
}
function ctrlMap(e: Config) {
mapConfig.value = e
}
</script> </script>
<template> <template>
<h5 class="logo">离线地图</h5> <h5 class="logo">离线地图</h5>
<Map ref="map" v-slot="{ contextmenu }" v-model="mapConfig"> <div class="main" ><router-view></router-view></div>
<q-menu touch-position context-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup>
<q-item-section @click="pushMark(contextmenu, 'localtion')">添加标点</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section @click="pushMark(contextmenu, 'equipment')">添加设备</q-item-section>
</q-item>
</q-list>
</q-menu>
<Mark v-for="(mark, i) in marks" :key="i" :latlng="mark.latlng" :title="mark.title" :opacity="mark.opacity"
:ref="`mark-${i}`">
<template v-if="mark.type === 'equipment'">
<q-icon :name="mdiLanConnect" size="md"></q-icon>
</template>
</Mark>
</Map>
<div class="bar"> <div class="bar">
<q-toggle class="leftctl" v-model="sideStyle" true-value="20em,logo" false-value="0em,bar" />
<ThemeSwitch></ThemeSwitch> <ThemeSwitch></ThemeSwitch>
</div> </div>
<div class="left "> <div class="left">
<q-toggle class="leftctl" v-model="sideStyle" true-value="20em,logo" false-value="0em,bar" /> <q-btn-toggle
<q-tabs v-model="tab" dense active-color="primary" indicator-color="primary" align="justify" narrow-indicator> v-model="nowRouter"
spread
class="my-custom-toggle"
no-caps
rounded
unelevated
toggle-color="primary"
color="white"
text-color="primary"
:options="[
{ label: '百度', value: '/baidu' },
{ label: '高德', value: '/gaode' },
{ label: '热力图', value: '/echart' },
]" />
<q-tabs v-model="tab" glossy>
<q-tab name="mark" label="标点" /> <q-tab name="mark" label="标点" />
<q-tab name="ctrl" label="控制" /> <q-tab name="ctrl" label="控制" />
</q-tabs> </q-tabs>
<q-tab-panels v-model="tab" animated keep-alive> <q-tab-panels v-model="tab" animated keep-alive>
<q-tab-panel name="mark"> <!-- <q-tab-panel name="mark">
<MarkMenu :marks="marks" @flyTo="flytoMark" @remove="removeMark"></MarkMenu> <MarkMenu :marks="marks" @flyTo="flytoMark" @remove="removeMark"></MarkMenu>
</q-tab-panel> </q-tab-panel>
<q-tab-panel name="ctrl"> <q-tab-panel name="ctrl">
<CtrlMenu @change="ctrlMap"></CtrlMenu> <CtrlMenu @change="ctrlMap"></CtrlMenu>
</q-tab-panel> </q-tab-panel> -->
</q-tab-panels> </q-tab-panels>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.logo { .logo {
margin: unset; margin: unset;
grid-area: var(--logoOrBar, logo); grid-area: var(--logoOrBar, logo);
will-change: filter; will-change: filter;
transition: filter 300ms; transition: filter 300ms;
align-self: center; align-self: center;
text-align: center; text-align: center;
} }
.logo:hover { .logo:hover {
filter: drop-shadow(0 0 2em #646cffaa); filter: drop-shadow(0 0 2em #646cffaa);
} }
.logo.vue:hover { .logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa); filter: drop-shadow(0 0 2em #42b883aa);
} }
.left { .left {
grid-area: left; grid-area: left;
overflow: auto; overflow: auto;
user-select: none; user-select: none;
.leftctl { .leftctl {
position: absolute; position: absolute;
top: 0px; top: 0px;
left: 10px; left: 10px;
} }
} }
.main {
grid-area: main;
position: relative;
}
.bar { .bar {
grid-area: bar; grid-area: bar;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
margin-right: 20px; margin-right: 20px;
} }
</style> </style>

View File

@ -1,54 +1,46 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, provide, watch } from 'vue' import { ref, onMounted, provide, watch } from 'vue';
import 'leaflet/dist/leaflet.css' import 'leaflet/dist/leaflet.css';
import useMap from '../hook/useMap.ts' import useMap from '../hook/useMap.ts';
import { useResizeObserver } from '@vueuse/core' import { useResizeObserver } from '@vueuse/core';
import { setOptions } from 'leaflet' import { setOptions } from 'leaflet';
const mapContainer = ref() const mapContainer = ref();
const { map, tileLayers, props: mapProps } = useMap(mapContainer, { use: 'gaode', zoom: 13 }); const { map, tileLayers, props: mapProps } = useMap(mapContainer, { use: 'gaode', zoom: 13 });
// const config = defineModel<Object>() // const config = defineModel<Object>()
const slotProps = ref({ const slotProps = ref({
contextmenu: {} contextmenu: {},
}) });
const mapFilter = ref('invert(1) grayscale(.2) saturate(0.8) brightness(1.8) opacity(1) hue-rotate(184deg) sepia(17%)'); // const mapFilter = ref('invert(1) grayscale(.2) saturate(0.8) brightness(1.8) opacity(1) hue-rotate(184deg) sepia(17%)'); //
provide('map', map); // mapChild
provide('map', map) // mapChild onMounted(() => {
map.value?.addEventListener('contextmenu', (e) => {
slotProps.value.contextmenu = e;
});
});
onMounted(() => { watch({ center: mapProps.value.center }, ({ center }) => center && map.value?.flyTo(center));
map.value?.addEventListener("contextmenu", (e) => { watch({ max: mapProps.value.maxZoom || 0 }, ({ max }) => map.value?.setMaxZoom(max));
slotProps.value.contextmenu = e watch({ min: mapProps.value.minZoom || 0 }, ({ min }) => map.value?.setMaxZoom(min));
}) watch({ url: mapProps.value.urlTemplate }, ({ url }) => url && tileLayers.value[0].setUrl(url));
}) watch({ zoomSnap: mapProps.value.zoomSnap }, ({ zoomSnap }) => map.value && (map.value.options.zoomSnap = zoomSnap));
function changeMapFilter(
filterInfo: Record<'blur' | 'brightness' | 'contrast' | 'drop-shadow' | 'grayscale' | 'hue-rotate' | 'invert' | 'opacity' | 'saturate' | 'sepia', string | number>
) {
mapFilter.value = Object.entries(filterInfo)
.map(([key, value]) => `${key}(${value})`)
.join(' ');
}
useResizeObserver(mapContainer, () => map.value?.invalidateSize(true));
watch({ center: mapProps.value.center }, ({ center }) => center && map.value?.flyTo(center)) defineExpose({ changeMapFilter, setOptions });
watch({ max: mapProps.value.maxZoom || 0 }, ({ max }) => map.value?.setMaxZoom(max))
watch({ min: mapProps.value.minZoom || 0 }, ({ min }) => map.value?.setMaxZoom(min))
watch({ url: mapProps.value.urlTemplate }, ({ url }) => url && tileLayers.value[0].setUrl(url))
watch({ zoomSnap: mapProps.value.zoomSnap }, ({ zoomSnap }) => map.value && (map.value.options.zoomSnap = zoomSnap))
function changeMapFilter(filterInfo: Record<'blur' |
'brightness' |
'contrast' |
'drop-shadow' |
'grayscale' |
'hue-rotate' |
'invert' |
'opacity' |
'saturate' |
'sepia', string | number>) {
mapFilter.value = Object.entries(filterInfo).map(([key, value]) => `${key}(${value})`).join(' ')
}
useResizeObserver(mapContainer, () => map.value?.invalidateSize(true))
defineExpose({ changeMapFilter, setOptions })
</script> </script>
<template> <template>
@ -58,13 +50,13 @@ defineExpose({ changeMapFilter, setOptions })
</template> </template>
<style lang="scss"> <style lang="scss">
.mapContainer { .mapContainer {
grid-area: map; grid-area: map;
} }
body.body--dark { body.body--dark {
.mapFilter { .mapFilter {
filter: var(--mapFilter); filter: var(--mapFilter);
} }
} }
</style> </style>

View File

@ -0,0 +1,43 @@
<template>
<q-menu touch-position context-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup>
<q-item-section @click="addPoint"></q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section @click="addDev"></q-item-section>
</q-item>
</q-list>
</q-menu>
</template>
<script setup lang="ts">
import { onMounted, PropType } from 'vue';
import { useMarkStore } from '../store/useMarkStore';
import { LeafletMouseEventHandlerFn } from 'leaflet';
const { marks } = useMarkStore();
const emit = defineEmits(['addPoint', 'addDev']);
const props = defineProps({ map: Object as PropType<L.Map> });
onMounted(() => {});
function addPoint() {
const { map } = props;
if (!map) return;
const pushMark: LeafletMouseEventHandlerFn = (e) => {
marks.push({ latlng: [e.latlng.alt || 0, e.latlng.lng || 0], type: 'localtion', opacity: 1, title: '新地标' });
};
map.addEventListener('contextmenu', pushMark);
return map.removeEventListener('contextmenu', pushMark);
}
function addDev() {
const { map } = props;
if (!map) return;
const pushMark: LeafletMouseEventHandlerFn = (e) => {
marks.push({ latlng: [e.latlng.alt || 0, e.latlng.lng || 0], type: 'equipment', opacity: 1, title: '新设备' });
};
map.addEventListener('contextmenu', pushMark);
return map.removeEventListener('contextmenu', pushMark);
}
</script>

View File

@ -5,15 +5,10 @@ export type DataProps = { zoom: { min: number, max: number }, urlTemplate: strin
<script lang="ts" setup> <script lang="ts" setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import useConfig from '../../hook/useConfig';
import { MapType } from '../../lib/mapType'; import { MapType } from '../../lib/mapType';
const _configGetter = useConfig()
const maptype = ref<MapType>('gaode') const maptype = ref<MapType>('gaode')
const data = ref(_configGetter('gaode'))
const emit = defineEmits(['change']) const emit = defineEmits(['change'])
watch(maptype, v => data.value = _configGetter(v))
watch(data, (v) => emit('change', v), { deep: true })
</script> </script>
<template> <template>
@ -30,11 +25,11 @@ watch(data, (v) => emit('change', v), { deep: true })
<q-item clickable v-close-popup> <q-item clickable v-close-popup>
<q-item-section> <q-item-section>
<q-item-label>缩放控制</q-item-label> <q-item-label>缩放控制</q-item-label>
<q-item-label caption> <!-- <q-item-label caption>
<q-range :model-value="{ max: data.maxZoom, min: data.minZoom }" <q-range :model-value="{ max: data.maxZoom, min: data.minZoom }"
@update:model-value="({ max, min }) => { data.maxZoom = max; data.minZoom = min; }" :min="3" @update:model-value="({ max, min }) => { data.maxZoom = max; data.minZoom = min; }" :min="3"
:max="19" color="deep-orange" label-always markers switch-label-side /> :max="19" color="deep-orange" label-always markers switch-label-side />
</q-item-label> </q-item-label> -->
<!-- TODO 控制多图层 ?? 缩放级别减少 直接控制路线等 --> <!-- TODO 控制多图层 ?? 缩放级别减少 直接控制路线等 -->
<!-- TODO 地图不清楚 --> <!-- TODO 地图不清楚 -->
</q-item-section> </q-item-section>
@ -43,7 +38,7 @@ watch(data, (v) => emit('change', v), { deep: true })
<q-item-section> <q-item-section>
<q-item-label>访问地址</q-item-label> <q-item-label>访问地址</q-item-label>
<q-item-label caption> <q-item-label caption>
<q-input v-model="data.urlTemplate" /> <!-- <q-input v-model="data.urlTemplate" /> -->
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -51,7 +46,7 @@ watch(data, (v) => emit('change', v), { deep: true })
<q-item-section> <q-item-section>
<q-item-label>失败图片</q-item-label> <q-item-label>失败图片</q-item-label>
<q-item-label caption> <q-item-label caption>
<q-input v-model="data.errorTileUrl" /> <!-- <q-input v-model="data.errorTileUrl" /> -->
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>
@ -59,7 +54,7 @@ watch(data, (v) => emit('change', v), { deep: true })
<q-item-section> <q-item-section>
<q-item-label>缩放倍数</q-item-label> <q-item-label>缩放倍数</q-item-label>
<q-item-label caption> <q-item-label caption>
<q-input v-model.number="data.zoomSnap" type="number" /> <!-- <q-input v-model.number="data.zoomSnap" type="number" /> -->
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>

39735
src/hook/data.ts 100644

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
import { MapType } from "../lib/mapType";
export type Config = L.TileLayerOptions & { urlTemplate: string, center: L.LatLngExpression, zoomSnap?: number }
const mapConfig: Record<MapType, Config> = {
baidu: {
urlTemplate: "http://localhost/baidumaps/roadmap/{z}/{x}/{y}.png",
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
center: [39.926474, 116.403283],
maxZoom: 15, minZoom: 3
},
gaode: {
urlTemplate: "http://localhost/mapabc/roadmap/{z}/{x}/{y}.png",
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
center: [-97.00238024827533, 210.7725501856634],
maxZoom: 15, minZoom: 3
},
gaodeLine: {
urlTemplate: "http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}",
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
center: [-97.00238024827533, 210.7725501856634],
maxZoom: 15, minZoom: 3
}
}
function useConfig(): (name: MapType) => Config {
return (name) => mapConfig[name]
}
export default useConfig

View File

@ -1,32 +1,29 @@
import L from 'leaflet' import L from 'leaflet';
import 'proj4' import 'proj4';
import 'proj4leaflet' import 'proj4leaflet';
import { MapType } from '../lib/mapType'; import { MapType } from '../lib/mapType';
const _level = 19;
const crss: Record<MapType, L.CRS> = { const crss: Record<MapType, L.CRS> = {
baidu: new L.Proj.CRS('EPSG:900913', baidu: new L.Proj.CRS('EPSG:900913', '+proj=merc +a=6378206 +b=6356584.314245179 +lat_ts=0.0 +lon_0=0.0 +x_0=0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs', {
'+proj=merc +a=6378206 +b=6356584.314245179 +lat_ts=0.0 +lon_0=0.0 +x_0=0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs', resolutions: (function () {
{ let level = 19;
resolutions: function () {
let level = 19
var res = []; var res = [];
res[0] = Math.pow(2, 18); res[0] = Math.pow(2, 18);
for (var i = 1; i < level; i++) { for (var i = 1; i < level; i++) {
res[i] = Math.pow(2, (18 - i)) res[i] = Math.pow(2, 18 - i);
} }
return res; return res;
}(), })(),
origin: [0, 0], origin: [0, 0],
bounds: L.bounds([20037508.342789244, 0], [0, 20037508.342789244]) bounds: L.bounds([20037508.342789244, 0], [0, 20037508.342789244]),
}), }),
gaode: L.CRS.Simple, gaode: L.CRS.Simple,
gaodeLine:L.CRS.EPSG4326 gaodeCharts: L.CRS.Simple,
} gaodeLine: L.CRS.EPSG4326,
};
function useCrs(name: MapType) { function useCrs(name: MapType) {
return crss[name] return crss[name];
} }
export default useCrs;
export default useCrs

View File

@ -1,6 +0,0 @@
function useDraw(){
}
export default useDraw

View File

@ -1,76 +1,32 @@
import L, { Layer } from "leaflet" import { use, init, ComposeOption } from 'echarts/core';
import { Ref, isRef, onMounted, reactive, ref, shallowRef, toRef, triggerRef, watch } from "vue"
import useCrs from "./useCrs"
import { MapType } from "../lib/mapType"
import useConfig, { Config } from "./useConfig"
/** import { ScatterChart, ScatterSeriesOption, EffectScatterChart, EffectScatterSeriesOption, HeatmapChart } from 'echarts/charts';
*
*/
interface MapController {
import { TooltipComponent, TitleComponentOption, TitleComponent, VisualMapComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { LeafletComponent, LeafletComponentOption } from '../leafletchart/export';
import L, { CRS, MapOptions } from 'leaflet';
import { HeatmapSeriesOption } from 'echarts';
export type EMapOptions = LeafletComponentOption<MapOptions & { getCrs?: () => CRS }>;
export type ECOption = ComposeOption<ScatterSeriesOption | EffectScatterSeriesOption | TitleComponentOption | HeatmapSeriesOption> & EMapOptions;
function useMap(dom: HTMLElement, chartOptions: ECOption) {
use([CanvasRenderer, TooltipComponent, LeafletComponent, ScatterChart, EffectScatterChart, TitleComponent, HeatmapChart, VisualMapComponent]);
const chart = init(dom);
chart.setOption(chartOptions);
// @ts-ignore
const lmapComponent = chart.getModel().getComponent('lmap');
// @ts-ignore
const lmap: L.Map = lmapComponent.getLeaflet();
return {
map: lmap,
chart,
};
} }
type DefineProps = L.MapOptions & Partial<Config> & { use: MapType } export default useMap;
interface DefineExport {
map: Ref<L.Map | undefined>
tileLayers: Ref<L.TileLayer[]>
gridLayers: Ref<L.GridLayer[]>
geoJsons: Ref<L.GeoJSON[]>
props: Ref<DefineProps>
triggerProps: (options: Config) => void
}
function useMap(dom: Ref<string | HTMLElement>, props: DefineProps | Ref<DefineProps>): DefineExport {
const _props = isRef(props) ? props : shallowRef(props)
const _map = ref<L.Map>()
const _tileLayers: Ref<L.TileLayer[]> = ref([])
const _gridLayers: Ref<L.GridLayer[]> = ref([])
const _geos: Ref<L.GeoJSON[]> = ref([])
const _configGetter = useConfig()
onMounted(() => {
const _options = _props // FIXME 这里必须要强转嘛
if (!_options) return
const crs = useCrs(_options.value.use)
const _config = _configGetter(_options.value.use)
_map.value = L.map(dom.value, { ..._options.value, crs, center: _config.center })
const layer = L.tileLayer(_config.urlTemplate, _config);
layer.addTo(_map.value)
_tileLayers.value.push(layer)
})
watch(_props, (options) => {
if (!_map.value) return
if (!options) return
_map.value.options.crs = useCrs(options.use)
})
watch(_tileLayers, (layers) => {
if (!_map.value) return
if (layers.length < 1) return
const { hasLayer: _has, addLayer: _add } = _map.value
layers.forEach(l => !_has(l) && _add(l))
})
watch(_geos, (geos) => {
const __map = _map.value
if (!__map) return
if (geos.length < 1) return
geos.forEach(geo => {
geo.addTo(__map)
})
})
/**
* props
*/
function triggerProps(options: Config) {
triggerRef(_props)
}
return { map: _map, props: _props, tileLayers: _tileLayers, gridLayers: _gridLayers, geoJsons: _geos, triggerProps }
}
export default useMap

View File

@ -1,12 +0,0 @@
import { useLocalStorage } from "@vueuse/core"
const key = 'map-marks'
export type markType = 'localtion' | 'equipment'
export type markInfo = { latlng: [number, number], title: string, opacity?: number, type?: markType }
function useMarks(initData: markInfo[] = []) {
const stroked = useLocalStorage<markInfo[]>(key, initData);
return stroked;
}
export default useMarks

View File

@ -0,0 +1,108 @@
import { data } from './data';
import { ECOption, EMapOptions } from './useMap';
function useOption() {
const heatmap: (mapOptions: EMapOptions) => ECOption = (mapOptions) => {
var points = [].concat.apply(
[],
data.map(function (track: any) {
return track.map(function (seg: { coord: number[] }) {
const [x, y] = seg.coord;
return [x - 3.73, y + 9.7, 1];
});
})
);
return {
animation: false,
lmap: {
center: [10, 60], // [lng, lat]
zoom: 4,
resizeEnable: true, // automatically handles browser window resize.
renderOnMoving: true,
echartsLayerInteractive: true, // Default: true
largeMode: false, // Default: false
...mapOptions?.leaflet,
},
title: {
text: '热力图',
},
visualMap: {
show: false,
top: 'top',
min: 0,
max: 5,
seriesIndex: 0,
calculable: true,
inRange: {
color: ['blue', 'blue', 'green', 'yellow', 'red'],
},
},
series: [
{
type: 'heatmap',
coordinateSystem: 'lmap',
data: points,
pointSize: 5,
blurSize: 6,
},
],
};
};
const scatter: (mapOptions?: EMapOptions) => ECOption = (mapOptions) => ({
lmap: {
center: [10, 60], // [lng, lat]
zoom: 4,
resizeEnable: true, // automatically handles browser window resize.
renderOnMoving: true,
echartsLayerInteractive: true, // Default: true
largeMode: false, // Default: false
...mapOptions?.leaflet,
},
title: {
text: '散点图',
},
series: [
{
type: 'scatter',
// use `lmap` as the coordinate system
coordinateSystem: 'lmap',
// data items [[lng, lat, value], [lng, lat, value], ...]
data: [
[39.926474, 116.403283, 400],
[10, 60, 8],
[10.1, 60, 20],
],
encode: {
// encode the third element of data item as the `value` dimension
value: 2,
},
},
{
type: 'effectScatter',
// use `lmap` as the coordinate system
coordinateSystem: 'lmap',
// data items [[lng, lat, value], [lng, lat, value], ...]
data: [
[39.926474, 116, 400],
[11, 60, 8],
[11.1, 60, 20],
],
encode: {
// encode the third element of data item as the `value` dimension
value: 2,
},
},
],
});
return {
scatter,
heatmap,
};
}
export default useOption;

View File

@ -1,69 +0,0 @@
type RGBA = Record<'r' | 'g' | 'b' | 'a', number>;
function imageChange(img: HTMLImageElement, colorize: (pixel: RGBA) => RGBA) {
if (img.getAttribute('data-colorized'))
return;
img.crossOrigin = 'anonymous'
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext("2d");
if (!ctx) return
ctx.drawImage(img, 0, 0);
var imgd = ctx.getImageData(0, 0, canvas.width, canvas.height);
var pix = imgd.data;
for (var i = 0, n = pix.length; i < n; i += 4) {
var pixel = colorize({ r: pix[i], g: pix[i + 1], b: pix[i + 2], a: pix[i + 3] });
if (!!!pixel || pixel !== Object(pixel) || Object.prototype.toString.call(pixel) === '[object Array]') {
if (i === 0) {
throw 'The colorize option should return an object with at least one of "r", "g", "b", or "a" properties.';
}
} else {
if (pixel.hasOwnProperty('r') && typeof pixel.r === 'number') {
pix[i] = pixel.r;
}
if (pixel.hasOwnProperty('g')) {
pix[i + 1] = pixel.g;
}
if (pixel.hasOwnProperty('b')) {
pix[i + 2] = pixel.b;
}
if (pixel.hasOwnProperty('a')) {
pix[i + 3] = pixel.a;
}
}
}
ctx.putImageData(imgd, 0, 0);
img.setAttribute('data-colorized', 'true');
img.src = canvas.toDataURL();
return imgd;
}
/**
*
* @param img
* @param colorize rgb
* @example
* const colorize = useRgb()
* layer.on('tileload', (e) => {
* colorize(e.tile, (pixel) => {
* pixel.g += 17;
* pixel.b += 90;
* return pixel;
* })
* })
* @returns
*/
function useRgb() {
return imageChange;
}
export default useRgb

View File

@ -0,0 +1,41 @@
import L from 'leaflet';
import { tileLayer as LtileLayer } from 'leaflet';
function useTileLayer() {
const baidu = LtileLayer('http://localhost/baidumaps/roadmap/{z}/{x}/{y}.png', {
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
maxZoom: 15,
tms: true,
minZoom: 3,
});
const gaode = LtileLayer('http://localhost/mapabc/roadmap/{z}/{x}/{y}.png', {
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
maxZoom: 15,
tms: true,
minZoom: 3,
});
const Kitten = L.TileLayer.extend({
getTileUrl: function ({ x, y, z }: any) {
return `http://localhost/mapabc/roadmap/${z}/${x}/${y}.png`;
},
options: {
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
maxZoom: 15,
minZoom: 3,
},
});
const gaodeChart = new Kitten();
const gaodeLine = LtileLayer('http://webrd02.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}', {
errorTileUrl: 'http://localhost/baidumaps/roadmap/error.png',
maxZoom: 15,
minZoom: 3,
});
return { baidu, gaode, gaodeLine, gaodeChart };
}
export default useTileLayer;

View File

@ -1,85 +0,0 @@
import L from "leaflet"
import { ref, Ref } from "vue";
function close(used: number[], close: number): number { // 接近数字下表
const closeUsed = used.map((v, i) => ({ v: close - v, i }))
const p = closeUsed.shift();
if (!p) throw new Error('传入使用数组不能为空');
const a = closeUsed.reduce((pre, cur) => {
if (pre.v < 0 && cur.v < 0) { // 双方小数
return pre.v < cur.v ? cur : pre // 取大
} else if (pre.v < 0 || cur.v < 0) { // 有一方是小数
return pre.v < 0 ? pre : cur // 取小数
} else { // 都不是
return pre.v < cur.v ? pre : cur // 取小
}
}, p)
return a.i
}
function limitZoom(min: number, max: number, zoom: number): number {
return Math.max(min, Math.min(max, zoom))
}
function useZoom(mapContainer: Ref<HTMLElement>, map: Ref<L.Map | undefined>, used: number[]) {
const _used = used.sort((a, b) => a - b)
let _timer = 0;
let _delta = 0;
let _currentZoom = ref(0);
const zoomControl = L.Handler.extend({
addHooks: function () {
console.log('add')
L.DomEvent.on(mapContainer.value, 'wheel', this._onWheelScroll)
},
removeHooks: function () {
console.log('remove')
L.DomEvent.off(mapContainer.value, 'wheel', this._onWheelScroll)
},
_onWheelScroll: function (e: WheelEvent) {
const _map = map.value;
if (!_map) return;
let _lastMousePos = _map.mouseEventToContainerPoint(e);
let _debounce = _map.options.wheelDebounceTime
const delta = L.DomEvent.getWheelDelta(e); // 处理高度
_delta += delta
clearTimeout(_timer)
_timer = setTimeout(() => {
let zoom = _map.getZoom(),
snap = _map.options.zoomSnap || 0,
d2 = _delta / ((_map.options.wheelPxPerZoomLevel || 60) * 4),
d3 = 4 * Math.log(2 / (1 + Math.exp(-Math.abs(d2)))) / Math.LN2,
d4 = snap ? Math.ceil(d3 / snap) * snap : d3,
min = _map.getMinZoom(),
max = _map.getMaxZoom(),
limit = limitZoom(min, max, zoom + (_delta > 0 ? d4 : -d4)),
delta = limit - zoom;
if (!delta) return
_currentZoom.value = close(_used, limit) + delta
const _to = _used[_currentZoom.value];
console.log(_used, '当前', zoom, '放大缩小', delta, '接近', close(_used, limit), '转到', _currentZoom.value, '实际', _to)
debugger
if (_map.options.scrollWheelZoom === 'center') {
_map.setZoom(_to);
} else {
_map.setZoomAround(_lastMousePos, _to);
}
_delta = 0
}, _debounce);
L.DomEvent.stop(e);
},
})
return zoomControl
}
export default useZoom

View File

@ -1,11 +0,0 @@
import { MapType } from "../lib/mapType";
function useConfig<T>( configName:T ,type: Record<MapType, T>) { // TODO 对config监听??
return // TODO 配置
}
export default useConfig

View File

@ -1,14 +0,0 @@
import L from 'leaflet'
import { Ref, onMounted, ref } from 'vue'
function useMap(mapContainer: Ref<string | HTMLElement>, options: L.MapOptions) {
const map = ref<L.Map>();
onMounted(() => {
map.value = L.map(mapContainer.value, options)
})
return [map]
}
export default useMap

View File

@ -1,26 +0,0 @@
import L from "leaflet";
import { ref, Ref, onMounted } from "vue";
function useTileLayer(map: Ref<L.Map>, url: string, options: L.TileLayerOptions) {
const tileLayer = ref<L.TileLayer>()
onMounted(() => {
tileLayer.value = L.tileLayer(url, options)
})
function open() {
if (!tileLayer.value) return;
if (!map.value) return;
tileLayer.value.addTo(map.value)
}
function close() {
if (!tileLayer.value) return;
if (!map.value) return;
tileLayer.value.removeFrom(map.value)
}
return [tileLayer, open, close]
}
export default useTileLayer

View File

@ -0,0 +1,246 @@
import { util as zrUtil, graphic, matrix } from 'echarts/core';
import { DomUtil, LatLng, Layer, Map as LMap, Projection } from 'leaflet';
import 'proj4';
import projleaflet from 'proj4leaflet';
function dataToCoordSize(dataSize, dataItem) {
dataItem = dataItem || [0, 0];
return zrUtil.map(
[0, 1],
function (dimIdx) {
const val = dataItem[dimIdx];
const halfSize = dataSize[dimIdx] / 2;
const p1 = [];
const p2 = [];
p1[dimIdx] = val - halfSize;
p2[dimIdx] = val + halfSize;
p1[1 - dimIdx] = p2[1 - dimIdx] = dataItem[1 - dimIdx];
return Math.abs(this.dataToPoint(p1)[dimIdx] - this.dataToPoint(p2)[dimIdx]);
},
this
);
}
// exclude private and unsupported options
const excludedOptions = ['echartsLayerInteractive', 'renderOnMoving', 'largeMode', 'layers', 'getCrs'];
const CustomOverlay = Layer.extend({
initialize: function (container) {
this._container = container;
},
onAdd: function (map) {
let pane = map.getPane(this.options.pane);
pane.appendChild(this._container);
// Calculate initial position of container with
// `L.Map.latLngToLayerPoint()`, `getPixelOrigin()
// and/or `getPixelBounds()`
// L.DomUtil.setPosition(this._container, point);
// Add and position children elements if needed
// map.on('zoomend viewreset', this._update, this);
},
onRemove: function (map) {
DomUtil.remove(this._container);
},
_update: function () {
// Recalculate position of container
// L.DomUtil.setPosition(this._container, point);
// Add/remove/reposition children elements if needed
},
});
function LeafletCoordSys(lmap, api) {
this._lmap = lmap;
this._api = api;
this._mapOffset = [0, 0];
this._projection = Projection.Mercator;
// this.dimensions = ['lat', 'lng'] // Is Leaflet default, but incompatible with echarts heatmap since isGeoCoordSys in HeatMapView.ts, checks for [0] === 'lng', [1] === 'lat'
}
const LeafletCoordSysProto = LeafletCoordSys.prototype;
LeafletCoordSysProto.setZoom = function (zoom) {
this._zoom = zoom;
};
LeafletCoordSysProto.setCenter = function (center) {
const latlng = this._projection.project(new LatLng(center[1], center[0])); // lng, lat
this._center = [latlng.lng, latlng.lat];
};
LeafletCoordSysProto.setMapOffset = function (mapOffset) {
this._mapOffset = mapOffset;
};
LeafletCoordSysProto.setLeaflet = function (lmap) {
this._lmap = lmap;
};
LeafletCoordSysProto.getLeaflet = function () {
return this._lmap;
};
LeafletCoordSysProto.dataToPoint = function (data) {
const latlng = new LatLng(data[1], data[0]); // lng, lat
const px = this._lmap.latLngToLayerPoint(latlng);
const mapOffset = this._mapOffset;
return [px.x - mapOffset[0], px.y - mapOffset[1]];
};
LeafletCoordSysProto.pointToData = function (pt) {
const mapOffset = this._mapOffset;
const coord = this._lmap.layerPointToLatLng({
x: pt[0] + mapOffset[0],
y: pt[1] + mapOffset[1],
});
return [coord.lng, coord.lat]; // lng, lat
};
LeafletCoordSysProto.getViewRect = function () {
const api = this._api;
return new graphic.BoundingRect(0, 0, api.getWidth(), api.getHeight());
};
LeafletCoordSysProto.getRoamTransform = function () {
return matrix.create();
};
LeafletCoordSysProto.prepareCustoms = function () {
const rect = this.getViewRect();
return {
coordSys: {
// The name exposed to user is always 'cartesian2d' but not 'grid'.
type: 'lmap',
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
api: {
coord: zrUtil.bind(this.dataToPoint, this),
size: zrUtil.bind(dataToCoordSize, this),
},
};
};
LeafletCoordSysProto.convertToPixel = function (ecModel, finder, value) {
// here we don't use finder as only one amap component is allowed
return this.dataToPoint(value);
};
LeafletCoordSysProto.convertFromPixel = function (ecModel, finder, value) {
// here we don't use finder as only one amap component is allowed
return this.pointToData(value);
};
LeafletCoordSys.create = function (ecModel, api) {
let lmapCoordSys;
ecModel.eachComponent('lmap', function (lmapModel) {
if (typeof L === 'undefined') {
throw new Error('Leaflet api is not loaded');
}
if (lmapCoordSys) {
throw new Error('Only one lmap echarts component is allowed');
}
let lmap = lmapModel.getLeaflet();
const echartsLayerInteractive = lmapModel.get('echartsLayerInteractive');
if (!lmap) {
const root = api.getDom();
const painter = api.getZr().painter;
let viewportRoot = painter.getViewportRoot();
viewportRoot.className = 'lmap-ec-layer';
// Not support IE8
let lmapRoot = root.querySelector('.ec-extension-leaflet');
if (lmapRoot) {
// Reset viewport left and top, which will be changed
// in moving handler in Leaflet View
viewportRoot.style.left = '0px';
viewportRoot.style.top = '0px';
root.removeChild(lmapRoot);
}
lmapRoot = document.createElement('div');
lmapRoot.className = 'ec-extension-leaflet';
lmapRoot.style.cssText = 'position:absolute;top:0;left:0;bottom:0;right:0;';
root.appendChild(lmapRoot);
const options = zrUtil.clone(lmapModel.get());
const crs = options.getCrs && options.getCrs();
// delete excluded options
zrUtil.each(excludedOptions, function (key) {
delete options[key];
});
if (crs) {
lmap = new LMap(lmapRoot, { ...options, crs });
} else {
lmap = new LMap(lmapRoot, { ...options });
}
/*
Encapsulate viewportRoot element into
the parent element responsible for moving,
avoiding direct manipulation of viewportRoot elements
affecting related attributes such as offset.
*/
let moveContainer = document.createElement('div');
moveContainer.style = 'position: relative;';
moveContainer.appendChild(viewportRoot);
new CustomOverlay(moveContainer).addTo(lmap);
lmapModel.setLeaflet(lmap);
lmapModel.setEChartsLayer(viewportRoot);
}
const oldEChartsLayerInteractive = lmapModel.__echartsLayerInteractive;
if (oldEChartsLayerInteractive !== echartsLayerInteractive) {
lmapModel.setEChartsLayerInteractive(echartsLayerInteractive);
lmapModel.__echartsLayerInteractive = echartsLayerInteractive;
}
const center = lmapModel.get('center');
const zoom = lmapModel.get('zoom');
if (center && zoom) {
const lmapCenter = lmap.getCenter(); // leaflet lat lng
const lmapZoom = lmap.getZoom();
const centerOrZoomChanged = lmapModel.centerOrZoomChanged(
[lmapCenter.lng, lmapCenter.lat], // lng, lat
lmapZoom
);
if (centerOrZoomChanged) {
lmap.setView(new LatLng(center[1], center[0]), zoom); // lng, lat
}
}
lmapCoordSys = new LeafletCoordSys(lmap, api);
lmapCoordSys.setMapOffset(lmapModel.__mapOffset || [0, 0]);
lmapCoordSys.setZoom(zoom);
lmapCoordSys.setCenter(center);
lmapModel.coordinateSystem = lmapCoordSys;
});
ecModel.eachSeries(function (seriesModel) {
if (seriesModel.get('coordinateSystem') === 'lmap') {
seriesModel.coordinateSystem = lmapCoordSys;
}
});
return lmapCoordSys && [lmapCoordSys];
};
LeafletCoordSysProto.dimensions = LeafletCoordSys.dimensions = ['lng', 'lat']; // lng, lat
LeafletCoordSysProto.type = 'lmap';
export default LeafletCoordSys;

View File

@ -0,0 +1,57 @@
import { ComponentModel } from 'echarts/core';
import { v2Equal } from './helper';
const LeafletModel = {
type: 'lmap',
setLeaflet(lmap) {
this.__lmap = lmap;
},
getLeaflet() {
return this.__lmap;
},
getId() {
return this.__lmap._leaflet_id;
},
setEChartsLayer(layer) {
this.__echartsLayer = layer;
},
getEChartsLayer() {
return this.__echartsLayer;
},
setEChartsLayerVisiblity(visible) {
this.__echartsLayer.style.display = visible ? 'block' : 'none';
},
// FIXME: NOT SUPPORT <= IE 10
setEChartsLayerInteractive(interactive) {
this.option.echartsLayerInteractive = !!interactive;
this.__echartsLayer.style.pointerEvents = interactive ? 'auto' : 'none';
},
setCenterAndZoom(center, zoom) {
// center received here is in lat, lng, so swap it
this.option.center = [center[1], center[0]];
this.option.zoom = zoom;
},
centerOrZoomChanged(center, zoom) {
const option = this.option;
return !(v2Equal(center, option.center) && zoom === option.zoom);
},
defaultOption: {
center: [10.39506, 63.43049], // lng, lat
zoom: 6,
echartsLayerInteractive: true,
renderOnMoving: true,
largeMode: false
}
};
export default ComponentModel.extend(LeafletModel);

View File

@ -0,0 +1,189 @@
import { ComponentView, getInstanceByDom, throttle } from 'echarts/core';
import { clearLogMap } from './helper';
const LeafletView = {
type: 'lmap',
init() {
this._isFirstRender = true;
},
render(lmapModel, ecModel, api) {
let rendering = true;
const lmap = lmapModel.getLeaflet();
const moveContainer = api.getZr().painter.getViewportRoot().parentNode;
const coordSys = lmapModel.coordinateSystem;
const offsetEl = lmap._mapPane;
const renderOnMoving = lmapModel.get('renderOnMoving');
const resizeEnable = lmapModel.get('resizeEnable');
const largeMode = lmapModel.get('largeMode');
let moveHandler = function(e) {
if (rendering) {
return;
}
let transformStyle = offsetEl.style.transform;
let dx = 0;
let dy = 0;
if (transformStyle) {
transformStyle = transformStyle.replace('translate3d(', '');
let parts = transformStyle.split(',');
dx = -parseInt(parts[0], 10);
dy = -parseInt(parts[1], 10);
} else {
// browsers that don't support transform: matrix
dx = -parseInt(offsetEl.style.left, 10);
dy = -parseInt(offsetEl.style.top, 10);
}
const mapOffset = [dx, dy];
const offsetLeft = mapOffset[0] + 'px';
const offsetTop = mapOffset[1] + 'px';
if (moveContainer.style.left !== offsetLeft) {
moveContainer.style.left = offsetLeft;
}
if (moveContainer.style.top !== offsetTop) {
moveContainer.style.top = offsetTop;
}
coordSys.setMapOffset(lmapModel.__mapOffset = mapOffset);
const actionParams = {
type: 'lmapRoam',
animation: {
// compatible with ECharts 5.x
// no delay for rendering but remain animation of elements
duration: 0
}
};
api.dispatchAction(actionParams);
};
if(largeMode) {
moveHandler = throttle(moveHandler, 20, true);
}
if(this._isFirstRender){
this._moveHandler = moveHandler;
}
let ctrlStartHandler = function(e) {
if(e.originalEvent.code === 'ControlLeft') {
lmap.dragging.disable();
moveHandler(e);
}
};
let ctrlEndHandler = function(e) {
if(e.originalEvent.code === 'ControlLeft') {
lmap.dragging.enable();
}
};
this._ctrlStartHandler = ctrlStartHandler;
this._ctrlEndHandler = ctrlEndHandler;
lmap.off('move', this._moveHandler);
lmap.off('moveend', this._moveHandler);
lmap.off('zoom', this._moveHandler);
lmap.off('viewreset', this._moveHandler);
lmap.off('zoomend', this._moveHandler);
lmap.off('keydown', this._ctrlStartHandler);
lmap.off('keyup', this._ctrlEndHandler);
if(this._ctrlStartHandler) {
lmap.off('keydown', this._ctrlStartHandler);
}
if(this._ctrlEndHandler) {
lmap.off('keyup', this._ctrlEndHandler);
}
if (this._resizeHandler) {
lmap.off('resize', this._resizeHandler);
}
if (this._moveStartHandler) {
lmap.off('move', this._moveStartHandler);
lmap.off('zoomstart', this._moveStartHandler);
lmap.off('zoom', this._moveStartHandler);
}
lmap.on(renderOnMoving ? 'move' : 'moveend', moveHandler);
lmap.on(renderOnMoving ? 'zoom' : 'zoomend', moveHandler);
if(renderOnMoving){
lmap.on('viewreset', moveHandler); // needed?
}
if(!renderOnMoving && !this._moveEndHandler) {
const moveEndHandler = function(e) {
setTimeout(function() {
lmapModel.setEChartsLayerVisiblity(true);
}, !largeMode ? 0 : 20);
};
this._moveEndHandler = moveEndHandler;
lmap.on('moveend', moveEndHandler);
lmap.on('zoomend', moveEndHandler);
}
lmap.on('keydown', ctrlStartHandler);
lmap.on('keyup', ctrlEndHandler);
this._moveHandler = moveHandler;
if (!renderOnMoving) {
const moveStartHandler = function() {
lmapModel.setEChartsLayerVisiblity(false);
setTimeout(function() {
lmapModel.setEChartsLayerVisiblity(true);
}, 500);
};
this._moveStartHandler = moveStartHandler;
lmap.on('move', moveStartHandler); // hide when move occurs
lmap.on('zoomstart', moveStartHandler); // hide when zoom starts
lmap.on('zoom', moveStartHandler); // hide when zoom occurs
}
if (resizeEnable) {
let resizeHandler = function() {
getInstanceByDom(api.getDom()).resize();
};
if (largeMode) {
resizeHandler = throttle(resizeHandler, 20, true);
}
this._resizeHandler = resizeHandler;
lmap.on('resize', resizeHandler);
}
this._isFirstRender = rendering = false;
},
dispose() {
clearLogMap();
const component = this.__model;
if (component) {
const leaflet = component.getLeaflet();
if(leaflet){
component.getLeaflet().off();
component.getLeaflet().remove();
}
component.setLeaflet(null);
component.setEChartsLayer(null);
if (component.coordinateSystem) {
component.coordinateSystem.setLeaflet(null);
component.coordinateSystem = null;
}
delete this._moveHandler;
delete this._resizeHandler;
delete this._moveStartHandler;
delete this._moveEndHandler;
delete this._ctrlEndHandler;
delete this._ctrlStartHandler;
}
}
};
export default ComponentView.extend(LeafletView);

42
src/leafletchart/export.d.ts vendored 100644
View File

@ -0,0 +1,42 @@
import { use } from "echarts/core";
import { HeatmapSeriesOption } from "echarts/charts";
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type LastOf<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never;
type Push<T extends any[], V> = [...T, V];
type TuplifyUnion<
T,
L = LastOf<T>,
N = [T] extends [never] ? true : false
> = true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>;
type EChartsExtensionInstallRegisters = Parameters<
TuplifyUnion<Parameters<typeof use>[0]>[0]
>[0];
export type EChartsExtensionRegisters = EChartsExtensionInstallRegisters;
// HeatmapSeriesOption does not support 'lmap'
type LeafletHeatmapSeriesOption = HeatmapSeriesOption & {
coordinateSystem: "lmap";
};
/**
* To install Leaflet component
* @param registers registers echarts registers.
*/
declare function install(registers: EChartsExtensionRegisters): void;
export * from "./types";
export { install as LeafletComponent, LeafletHeatmapSeriesOption };

View File

@ -0,0 +1 @@
export { LeafletComponent } from './lmap'

View File

@ -0,0 +1,19 @@
import { version } from "echarts/core"; //lib/echarts";
export const ecVer = version.split('.');
export function v2Equal(a, b) {
return a && b && a[0] === b[0] && a[1] === b[1];
}
let logMap = {};
export function logWarn(tag, msg, once) {
const log = `[ECharts][Extension][Leaflet]${tag ? ' ' + tag + ':' : ''} ${msg}`;
once && logMap[log] || console.warn(log);
once && (logMap[log] = true);
}
export function clearLogMap() {
logMap = {};
}

1
src/leafletchart/index.d.ts vendored 100644
View File

@ -0,0 +1 @@
export * from "./types";

View File

@ -0,0 +1,4 @@
import { use } from 'echarts/core';
import { LeafletComponent } from './lmap';
use(LeafletComponent);

View File

@ -0,0 +1,56 @@
/**
* Leaflet component extension
*/
import LeafletCoordSys from './LeafletCoordSys';
import LeafletModel from './LeafletModel';
import LeafletView from './LeafletView';
import { ecVer } from "./helper";
function install(registers) {
// add coordinate system support for pie series for ECharts < 5.4.0
if ((ecVer[0] == 5 && ecVer[1] < 4)) {
registers.registerLayout(function(ecModel, api) {
ecModel.eachSeriesByType('pie', function (seriesModel) {
const coordSys = seriesModel.coordinateSystem;
const data = seriesModel.getData();
const valueDim = data.mapDimension('value');
if (coordSys && coordSys.type === 'lmap') {
const center = seriesModel.get('center');
const point = coordSys.dataToPoint(center);
const cx = point[0];
const cy = point[1];
data.each(valueDim, function (value, idx) {
const layout = data.getItemLayout(idx);
layout.cx = cx;
layout.cy = cy;
});
}
});
});
}
registers.registerComponentModel(LeafletModel);
registers.registerComponentView(LeafletView);
registers.registerCoordinateSystem('lmap', LeafletCoordSys);
// Action
registers.registerAction(
{
type: 'lmapRoam',
event: 'lmapRoam',
update: 'updateLayout'
},
function(payload, ecModel) {
ecModel.eachComponent('lmap', function(lmapModel) {
const lmap = lmapModel.getLeaflet();
const center = lmap.getCenter();
lmapModel.setCenterAndZoom([center.lat, center.lng], lmap.getZoom());
});
}
);
}
export {install as LeafletComponent}

36
src/leafletchart/types.d.ts vendored 100644
View File

@ -0,0 +1,36 @@
declare const name = "echarts-extension-leaflet";
declare const version = "1.0.0";
interface InnerLeafletComponentOption {
/**
* Whether echarts layer is interactive.
* @default true
* @since v1.0.0
*/
echartsLayerInteractive?: Boolean;
/**
* Whether to enable large mode
* @default false
* @since v1.0.0
*/
largeMode?: false;
/**
* Whether echarts layer should be rendered when the map is moving.
* if `false`, it will only be re-rendered after the map `moveend`.
* It's better to set this option to false if data is large.
* @default true
*/
renderOnMoving?: boolean;
}
/**
* Extended Leaflet component option
*/
interface LeafletComponentOption<LeafletOption> {
leaflet?: LeafletOption extends never
? InnerLeafletComponentOption
: InnerLeafletComponentOption & LeafletOption;
}
export { name, version, LeafletComponentOption };

View File

@ -1 +1 @@
export type MapType = 'baidu' | 'gaode' | 'gaodeLine' export type MapType = 'baidu' | 'gaode' | 'gaodeLine' | 'gaodeCharts';

17
src/lib/router.ts 100644
View File

@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router';
import baidu from '../pages/baidu/index.vue';
import gaode from '../pages/gaode/index.vue';
import echart from '../pages/echart/index.vue';
const routes = [
{ path: '/', redirect: '/gaode' },
{ path: '/gaode', component: gaode },
{ path: '/baidu', component: baidu },
{ path: '/echart', component: echart },
];
export const router = createRouter({
history: createWebHistory(),
routes,
});

View File

@ -1,20 +1,25 @@
import { createApp } from 'vue' import { createApp } from 'vue';
import './style.scss' import './style.scss';
import App from './App.vue' import App from './App.vue';
import { Quasar } from 'quasar' import { Quasar } from 'quasar';
import { router } from './lib/router';
// Import icon libraries // Import icon libraries
import '@quasar/extras/material-icons/material-icons.css' import '@quasar/extras/material-icons/material-icons.css';
import '@quasar/extras/mdi-v7/mdi-v7.css' import '@quasar/extras/mdi-v7/mdi-v7.css';
// Import Quasar css // Import Quasar css
import 'quasar/src/css/index.sass' import 'quasar/src/css/index.sass';
import { createPinia } from 'pinia';
const myApp = createApp(App);
const myApp = createApp(App) const pinia = createPinia();
myApp.use(Quasar, { myApp.use(Quasar, {
plugins: {}, // import Quasar plugins and add here plugins: {}, // import Quasar plugins and add here
}) });
myApp.mount('#app') myApp.use(pinia);
myApp.use(router);
myApp.mount('#app');

View File

@ -0,0 +1,30 @@
<template>
<div ref="mapContainer" style="width: 100%; height: 100%">
<MapMenu :map="map"></MapMenu>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useTileLayer from '../../hook/useTileLayer';
import useCrs from '../../hook/useCrs';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import MapMenu from '../../components/MapMenu.vue';
const mapContainer = ref<HTMLElement>();
const { baidu } = useTileLayer();
const map = ref<L.Map>();
onMounted(() => {
if (!mapContainer.value) return;
map.value = L.map(mapContainer.value, {
crs: useCrs('baidu'),
center: [39.926474, 116.403283],
zoom: 10,
maxZoom: 15,
minZoom: 3,
});
baidu.addTo(map.value);
});
</script>

View File

@ -0,0 +1,33 @@
<template>
<div ref="mapContainer" style="width: 100%; height: 100%">
<q-menu touch-position context-menu>
<q-list dense style="min-width: 100px">
<q-item clickable v-close-popup>
<q-item-section @click="$emit('flyTo')"></q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section @click="$emit('remove')"></q-item-section>
</q-item>
</q-list>
</q-menu>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useOption from '../../hook/useOption';
import useMap from '../../hook/useMap';
import useTileLayer from '../../hook/useTileLayer';
import 'leaflet/dist/leaflet.css';
import { useResizeObserver } from '@vueuse/core';
const mapContainer = ref<HTMLElement>();
const { heatmap } = useOption();
const { gaodeChart } = useTileLayer();
onMounted(() => {
if (!mapContainer.value) return;
const { map } = useMap(mapContainer.value, heatmap({ leaflet: { center: [116.396226, 39.914797], zoom: 13 } }));
gaodeChart.addTo(map);
useResizeObserver(mapContainer, () => map.invalidateSize(true));
});
</script>

View File

@ -0,0 +1,25 @@
<template>
<div ref="mapContainer" style="width: 100%; height: 100%" ></div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useTileLayer from '../../hook/useTileLayer';
import L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import useCrs from '../../hook/useCrs';
const mapContainer = ref<HTMLElement>();
const { gaode } = useTileLayer();
onMounted(() => {
if (!mapContainer.value) return;
const map = L.map(mapContainer.value, {
crs: useCrs('gaode'),
center: [-97.00238024827533, 210.7725501856634],
zoom: 10,
maxZoom: 15,
minZoom: 3,
});
gaode.addTo(map);
});
</script>

View File

@ -0,0 +1,14 @@
import { useLocalStorage } from '@vueuse/core';
import { defineStore } from 'pinia';
const key = 'map-marks';
export type markType = 'localtion' | 'equipment';
export type markInfo = { latlng: [number, number]; title: string; opacity?: number; type?: markType };
export const useMarkStore = defineStore('mark', () => {
const marks = useLocalStorage<markInfo[]>(key, []);
return {
marks,
};
});

View File

@ -2,8 +2,9 @@
display: grid; display: grid;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
grid-template-areas: "logo bar" grid-template-areas:
"left map"; 'logo bar'
'left main';
grid-template-rows: 3em 1fr; grid-template-rows: 3em 1fr;
grid-template-columns: var(--side, 20em) 1fr; grid-template-columns: var(--side, 20em) 1fr;
} }