全栈从0到1 3D旅游地图标记和轨迹生成

news2024/11/19 3:23:45

功能演示

演示视频

image.png

image.png

体验地址

Vercel App

开发技术栈:

  • NextJs(前端框架)
  • React(前端框架)
  • TailwindCSS (CSS样式)
  • echart + echart gl (地图生成)
  • shadui(UI组件库)
  • Zustand
  • lucide-react (图标)

第三方:

  • Convex(数据存储+接口)
  • Vercel(项目托管)
  • 高德开放平台(提供地图编码、逆编码等WEB API)

开发流程

下面给出关键步骤及部分代码。

1. Setup

1.1 初始化NextJS项目

系统要求:Nodejs 18.17+
打开终端,在控制台执行:

npx create-next-app@latest```
  
全部选择默认选项即可。  
![image.png](https://cdn.jsdelivr.net/gh/Ygria/Pictures@main/20240425232407.png)  
  
初始化完成后,进入项目并运行。  
  
```bash  
cd travel-tracel  
npm run dev  
  

打开localhost:3000,看到如下页面,项目初始化成功。
image.png

1.2 安装npm依赖

安装一些主要的依赖。后续需要用到的依赖可以边开发边安装。

# 安装echarts依赖  
npm install echarts-for-react  
npm install echarts-gl  
npm install require # 图标组件  
npm install lucide-react  

2 定义标记点

实现输入地点或地点关键字,查询经纬度,从而完成标记点的定义。
实现思路:

  1. 首先查询Convex数据库,提供10个最匹配的候选项
  2. 若没有找到对应候选项,用户可以点击搜索按钮,会调用高德API进行查询。
  3. 如果仍然没有查询到,可以点击“经纬度”按钮,进行经纬度的自行填写。
  4. 已经添加了的标记点,支持编辑和删除。
  5. 使用Zustand,进行地点增、删、改等状态管理。

2.1 定义useLocationPoints store

import {create} from "zustand";  import {nanoid} from "nanoid";  interface ILocationPoints {    
    locations: LocationPoint[],    
    addLocation: (loc: LocationPoint) => void,    
    editLocation: (loc: LocationPoint) => void,    
removeLocation: (id: string) => void  }  interface LocationPoint{    
    id: string,    
    name: string,    
    lng: string,    
lat: string  }    
    
export const useLocationPoints = create<ILocationPoints>(set => ({    
    locations: [] as LocationPoint[],    
    addLocation: (loc: LocationPoint) => {    
        const _id  = nanoid()    
        loc["id"] = _id    
        set(state => ({    
            locations: [...state.locations, loc]    
        }));    
    },    
    
    editLocation: (loc: LocationPoint)=>{    
        set(state => ({    
            locations: state.locations.map(item=>{    
                if(item.id === loc.id){    
                    return {    
                        ...loc,    
                        id: item.id,    
                    }    
                }else {    
                    return item    
                }    
            })    
        }));    
    
    },    
// 移除location    
    removeLocation:(id:string)=>{    
        set(state => ({    
            locations: state.locations.filter(item=>item.id !== id)    
        }));    
}  }));  

对于location的增删改,均依赖于以上store实现。单页面定义React自带的useState当然也可以,但是为了便利于组件拆分,所以使用store,不用再跨组件管理状态的提交、更新、监听等,方便了很多。
考虑到location的名称、经纬度均有可能更改,所以使用nanoid生成唯一id进行location的索引。

2.2 引入Convex

使用convex作为平台后台。convex可以提供数据库存储、RESTful接口及接口调试的功能。

2.2.1 Convex 项目配置
  1. 访问Convex

  2. 创建app
    image.png

  3. 在nextjs项目中,进行相应的配置。
    参考Convex官方文档:
    Next.js Quickstart | Convex Developer Hub

# 进入项目目录后,安装  
cd my-app && npm install convex  
npx convex dev  

因为我已经在Convex控制台中创建过app了,所以选择已存在的project

image.png

2.2.2 Table Schema定义和数据初始化

在Convex目录下,新建·文件schema.ts

import { v } from "convex/values";  
import {defineSchema, defineTable} from "convex/server";  
  
  
export default defineSchema({  
    locations: defineTable({        // 经纬度  
        name: v.string(),        //  经度  
        lng: v.number(),        // 纬度  
        lat:v.number(),  
    })        .index("by_name",["name"])        .searchIndex("search_by_name",{            searchField: "name",            filterFields: ["name"]        }),});  

如上所示,定义了一个名称为“locations”的数据表,有name、lng、lat三个字段,并定义了查询的规则(by_name)
运行npx convex dev后,会发现Convex控制台中已经生成了该表。
我从网上找到了一些世界范围内的经纬度数据,是csv格式。通过python处理成出初始化数据。
小技巧:对于不同格式的csv,在第一行定义与字段相匹配的表头即可快速处理。

import json    
    
import pandas as pd    
    
data = []  csv_data = pd.read_csv('globalcities.csv',header=0,encoding="utf-8")    
    
# {"name": "上海", "lng":  121.47,"lat":31.23}  with open("global.csv","w",encoding='utf-8') as file:    
      
    for index, row in csv_data.iterrows():    
        d = {    
            "name": row["城市名中文"] ,    
            "lng":row["经度"],    
            "lat":row["纬度"]    
        }    
        file.write(json.dumps(d,ensure_ascii=False) )    
        print(json)  
  

执行导入:

npx convex import --table locations convex/init.jsonl```
  
如下图所示,数据导入完成!  
  
![image.png](https://cdn.jsdelivr.net/gh/Ygria/Pictures@main/20240425220013.png)  
  
#### 2.2.3 查询接口定义和使用  
  
在convex文件夹下,新建文件location.ts,定义一个get查询接口  
```javascript  
import { v } from "convex/values";  import { query } from "./_generated/server"    
    
export const get = query({    
    args:{    
        // orgId: v.string(),    
        // search: v.optional(v.string()),        // favorites: v.optional(v.string())        name: v.string()    
    },    
    handler: async (ctx,args) => {    
    
        let locations = [];    
    
        locations = await ctx.db.query("locations")    
            .withSearchIndex("search_by_name", (q) =>    
            q.search("name", args.name)).take(10);    
    
    
        return locations;    
}  })  

运行npx convex dev进行接口的生成。
接口使用:(value为useState定义的动态值,绑定地点input输入框)

const queryResult =  useQuery(api.locations.get, {name: value});  

在模版代码中遍历查询结果:

{    
    queryResult?.map(res => (    
        <Badge variant="outline" key = {res._id} onClick={event => handleClick(event, res)}>    
            {res.name } [<span className = "text-red-300">{res.lng}</span>,<span className = "text-green-800">{res.lat}</span>]    
        </Badge>    
))  }  

实现效果如下图所示,输入内容后value更新,就会触发接口调用,出现候选地点供用户选择。
image.png

2.3 引入高德API

由于上一步的地点不一定全,也由于限定了仅显示前10条,故而又引入高德api进行查询。(有局限:无法查询国外地名)

2.3.1 高德开放平台
  1. 新建应用

image.png

  1. 创建key(web端使用的key)

image.png

使用该配置好的key就可以调用高德的接口了,每天有免费五千次的额度,对于一个小demo完全够用了。  
2.3.2 调用高德接口

gaode.ts

// 调用接口    
   
interface GaodeRes {    
   formatted_address: string,    
location: string  }    
     
export const getGeoCode = async (address: string) : Promise<GaodeRes[]> => {    
   let result = await fetch(`https://restapi.amap.com/v3/geocode/geo?address=${address}&key={}`,{    
       headers: {    
           Accept: 'application/vnd.dpexpo.v1+json' //设置请求头    
       },    
       method: 'get',    
   })    
   let res = await result.json() //必须通过此方法才可返回数据    
return res.geocodes;  }  

该接口设定为点击查询按钮时才触发(节约次数)。

const [gaodeQueryResult,setGaodeQueryResult] = useState([]);  
  
const searchGeoCode = () =>{    
    let queryResult = getGeoCode(value);    
    queryResult.then(res=>{    
        if(res && res.length > 0){    
            let data  = res.map(item=>{    
                return {    
                    "name": item.formatted_address,    
                    "lng": item.location.split(",")[0],    
                    "lat": item.location.split(",")[1],    
                }    
            })    
    
            setGaodeQueryResult(data)    
        }else{    
            setGaodeQueryResult([])    
            toast.error("未能查询到该地点!您可以通过经纬度进行查询。")    
        }    
    })    
    
}  

同样将结果在模版代码中遍历展示即可。

2.4 通过经纬度增加

如果查询不到,实现了点击“经纬度”展开,自行输入经纬度定义标记点的功能。值得一提的是使用了ShadUI的InputOTP组件,可以规定输入的位数和正则,我规定了只可以输入负号、小数点和数字。

image.png

2.5 标记点的删除和编辑

  1. 悬停状态才显示编辑和删除按钮。
<div className = "flex gap-x-2 m-2 relative group" ref={drag}    
      style={{    
          opacity: isDragging ? 0.5 : 1,    
      }}><MapPin />{name}    
    <button className = "opacity-0 group-hover:opacity-100" onClick={()=>onOpen(id,name,lng,lat)} ><Pencil size = "16"></Pencil> </button>    
<button className = "opacity-0 group-hover:opacity-100" onClick={onRemove} ><X size = "16"></X> </button>  </div>  
  1. 定义useEditModal,控制编辑Modal的显示和方法。
import {create} from "zustand";  const defaultValues = {    
    name: "",    
    lng: "",    
    lat: "",    
id: ""  };    
    
    
interface IEditModal {    
    isOpen: boolean;    
    initialValues: typeof defaultValues;    
    onOpen: (id:string,name:string,lng: string,lat: string) =>void;    
onClose: () => void;  }    
    
    
export const useEditModal = create<IEditModal>((set) =>({    
    isOpen: false,    
    onOpen:(id:string,name,lng,lat)=>set({    
        isOpen:true,    
        initialValues: {id,name,lng,lat}    
    }),    
    onClose: ()=>set({    
        isOpen: false,    
        initialValues: defaultValues    
    }),    
    initialValues: defaultValues    
    
}))  

image.png

2.2 定义路线(react dnd)

用户可以拖拽地点到虚线框内,形成路线。路线图的增删改同样适用zustand实现,不加赘述。拖动点到路线框中形成路线,使用了react drag and drop库完成。

npm install react-dnd```
引入react dnd后,将使用到拖拽的部分使用如下provider包裹。  
```html  
<DndProvider backend={HTML5Backend}></DndProvider>  

参考官方示例写法,部分:(location.tsx)

import { useDrag } from 'react-dnd'  
const [{ isDragging, }, drag, preview] = useDrag(    
    () => ({    
        type: ItemTypes.Location,    
        item: { name: name,id:id } ,    
    
        collect: (monitor) => ({    
            isDragging: !!monitor.isDragging(),    
        }),    
    }),    
[],  )  

部分:

import {Overlay, OverlayType} from "@/app/components/Overlay";  
import { useDrop} from 'react-dnd'  
const [{ isOver,canDrop }, drop] = useDrop(    
    () => ({    
        accept: ItemTypes.Location,    
        canDrop: (item:{name: string,id:string}) => {    
            if(!lineData || lineData.length == 0){    
                return true    
            }else{    
                return lineData[lineData.length - 1]?.id !== item.id    
            }    
        },    
        drop: (item:{name: string,id:string}) => {    
    
            dropLocation(id, item)    
    
        },    
    
        collect: (monitor) => ({    
            isOver: !!monitor.isOver(),    
            canDrop: !!monitor.canDrop(),    
        }),    
    }),    
[lineData],  )  

在模版代码中,增加了Overlay并根据是否可以放的状态,给不同的颜色。

{isOver && !canDrop && <Overlay type={OverlayType.IllegalMoveHover} />}  {!isOver && canDrop && <Overlay type={OverlayType.PossibleMove} />}  {isOver && canDrop && <Overlay type={OverlayType.LegalMoveHover} />}  

Overlay.js

export var OverlayType  ;(function (OverlayType) {    
    OverlayType['IllegalMoveHover'] = 'Illegal'    
    OverlayType['LegalMoveHover'] = 'Legal'    
OverlayType['PossibleMove'] = 'Possible'  })(OverlayType || (OverlayType = {}))  export const Overlay = ({ type }) => {    
    const color = getOverlayColor(type)    
    return (    
        <div    
            className="overlay"    
            role={type}    
            style={{    
                position: 'absolute',    
                top: 0,    
                left: 0,    
                height: '100%',    
                width: '100%',    
                zIndex: 1,    
                opacity: 0.5,    
                backgroundColor: color,    
            }}    
        />    
)  }  function getOverlayColor(type) {    
    switch (type) {    
        case OverlayType.IllegalMoveHover:    
            return 'red'    
        case OverlayType.LegalMoveHover:    
            return 'green'    
        case OverlayType.PossibleMove:    
            return '#66CC66'    
}  }  

当所拖拽的地点与路线合集中最后一个地点一样时,不允许拖拽进入。
image.png

3 地图渲染(echartgl)

使用react-echart,并导入echart-gl,实现3D地图渲染。

<ReactEcharts    
    option={options}    
    style={{ width: "900px", height: "800px" }}    
    
></ReactEcharts>  
  const [options,setOptions] = useState({    
    backgroundColor: "#000",    
    globe: {    
        baseTexture:"/earth1.jpg",    
        shading: "lambert",    
        atmosphere: {    
// 不需要大气光圈去掉即    
            show: false,    
offset: 4, // 大气层光圈宽度    
        },    
        viewControl: {    
distance: 200, // 默认视角距离地球表面距离    
        },    
        light: {    
            ambient: {    
intensity: 1, // 全局的环境光设置    
            },    
            main: {    
intensity: 1, // 场景主光源设置    
            },    
        },    
    },    
    
})  

使用useState,根据lineCollection、location数据,动态地增、减地图options。

useEffect(() => {    
   let series = initSeries;    
    series[0].data = normalData(lines);    
    series[1].data = activeData(lines);    
    locations.forEach((item) => {    
        series[2].data.push({    
            name: item.name,    
            value: [item.lng,item.lat]    
        });    
    });    
    setOptions({    
        ...options,    
        ...customTheme,    
        series: series    
    })    
    
}, [locations,lineCollections,customTheme]);  

3.1 地球换皮肤

更换贴图,即可实现地球换皮肤。
从网上搜罗一些地球贴图,放入public目录即可。
image.png

const themeTopics  = [{    
    globe: {    
        baseTexture: "/earth1.jpg",    
      
},  },    
    {    
      
        globe: {    
            baseTexture: "/earth2.jpg",    
            
        },    
    },    
    {        
        globe: {    
            baseTexture: "/earth3.jpg",         
        },    
    },    
    {         
        globe: {    
            baseTexture: "/earth4.jpg",     
        },    
}  ]  

部署

使用vercel作为部署托管。进入vercel并授权github项目,配置NextJS项目的构建命令。
由于我在github的项目源码没有放在根目录,所以还需要设置root-directory。

image.png
将所使用到的环境变量放在environment-variables中。
image.png

image.png
需要注意的是,Convex需要生成部署生产使用的URL和KEY,并配到环境变量中。
image.png

这样就完成啦~

源码地址

https://github.com/Ygria/travel-trace

小结

写的第一个相对完整的react小项目,麻雀虽小五脏俱全。使用合理的开源组件让全栈变得非常容易。
只使用到了react useState和useEffect两个hooks,已经感觉到了一定的理解门槛,与vue的将许多状态处理都放在内部封装好相比,react很多时候需要你自己来理解状态的依赖关系然后处理。react的tsx函数式写法的确很方便(比vue的defineComponents好多了……)。期待随着学习深入,了解到更多有趣的东西。

参考

  1. echart assets
    https://github.com/ecomfe/echarts-gl/tree/master/test/asset
  2. 全流程开发参考:
    https://www.codewithantonio.com/courses/88ee3ccc-afd7-414b-a626-e59c93847f65/chapters/b2fb3143-9683-465d-ad49-04f92011a107
  3. echarts+echarts-gl实现带有散点、路径的3d地球
    https://download.csdn.net/download/weixin_45669156/86248540?ydreferer=aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTY2OTE1Ni9hcnRpY2xlL2RldGFpbHMvMTI1OTMyNjAx

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1624732.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

机器视觉系统-工业光源什么是无影光

光路描述&#xff1a;通过结构或漫射板改变光路&#xff0c;最终发光角度包含了高角度 和低角度。 效果分析&#xff1a;兼具了高角度光和低角度光的效果&#xff0c;使被测物得到了多角度的照射&#xff0c;表面纹理、皱褶被弱化&#xff0c; 图像上整体均匀。 主要应用&#…

linux 上 jps 列出一堆 jar,如何快速定位 jar 文件启动位置?

例如&#xff0c;在 /data下有一个 xxx.jar &#xff0c;如果是通过 "java -jar /data/xxx.jar" 方式启动&#xff0c;则 jps会列出的名字中带 xxx.jar&#xff0c;这时再 "ps -ef | grep xxx.jar" 就会列出 更详细的信息&#xff0c;例如 "java -ja…

Spring Kafka——基于 Spring Kafka 实现动态管理 Kafka 连接和 topic 的监听

文章目录 使用 Spring Kafka 动态管理 Kafka 连接和主题监听1. 前言2. 简单的消费程序配置3. Spring Kafka 主要的相关类的说明4. KafkaListener 注解的加载执行流程解析5. 动态监听消费订阅的设计与实现 使用 Spring Kafka 动态管理 Kafka 连接和主题监听 文章内容较长&#x…

Windows电脑中护眼(夜间)模式的开启异常

我的电脑是联想小新16pro&#xff0c;Windows11版本。之前一直可以正常使用夜间模式&#xff0c;但是经过一次电脑的版本更新之后&#xff0c;我重启电脑发现我的夜间模式不能使用了。明明显示开启状态&#xff0c;但是却不能使用&#xff0c;电脑还是无法显示夜间模式。 询问…

59、回溯-括号生成

思路&#xff1a; 括号是成对出现&#xff0c;首先左括号可以放n个&#xff0c;右括号也可以放n个。如果当前左括号放了3了&#xff0c;右括号放了4个&#xff0c;错了&#xff0c;如果左括号放了5个&#xff0c;右括号放了4个。可以&#xff0c;继续放右括号即可。所以可以设…

每日一题:跳跃游戏II

给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处: 0 < j < nums[i] i j < n 返回到达 nums[n - 1] 的最…

【Linux系统化学习】死锁 | 线程同步

目录 死锁 死锁的必要条件 避免死锁 线程同步 条件变量 同步概念和竞态条件 条件变量接口 创建和初始化条件变量 等待条件满足 唤醒等待 毁条件变量 为什么 pthread_cond_wait 需要互斥量? 条件变量使用规范 等待条件代码 给条件发送信号代码 死锁 死锁是指在一…

深度探讨容器化技术在网络安全中的应用与挑战

随着容器化技术的快速发展&#xff0c;尤其是Docker与Kubernetes&#xff08;K8s&#xff09;的广泛应用&#xff0c;企业IT架构正经历着从传统虚拟机向轻量级容器的深刻变革。容器化技术为提升资源利用率、加速应用部署及维护提供了强大支持&#xff0c;但同时也给网络安全带来…

用 VMare Workstation 搭建 esxi --- (一)创建 exsi 虚拟机

用 VMare Workstation 搭建 esxi 文章目录 用 VMare Workstation 搭建 esxi创建虚拟机 创建虚拟机

企业微信代开发应用登录操作

首先声明&#xff1a;企微的文档写得真烂&#xff01;&#xff01;&#xff01;有一些问题&#xff0c;官方情愿在问答区给用户一个个解答&#xff0c;也不愿意在文档写清楚&#xff0c;生怕自己工作量不饱和被优化。 概念说明 代开发应用&#xff0c;是相对于自建应用来说的。…

计算机网络和因特网

Internet: 主机/端系统&#xff08;end System / host&#xff09;&#xff1a; 硬件 操作系统 网络应用程序 通信链路&#xff1a; 光纤、网络电缆、无线电、卫星 传输效率&#xff1a;带宽&#xff08;bps&#xff09; 分组交换设备&#xff1a;转达分组 包括&#…

Centos的一些基础命令

CentOS是一个基于开源代码构建的免费Linux发行版&#xff0c;它由Red Hat Enterprise Linux (RHEL) 的源代码重新编译而成。由于 CentOS是基于RHEL构建的&#xff0c;因此它与RHEL具有非常类似的特性和功能&#xff0c;包括稳定性、安全性和可靠性。并且大部分的 Linux 命令在C…

SpringBoot学习之Redis下载安装启动【Mac版本】(三十七)

一、下载Redis 1、下载地址:Downloads - Redis 往下滑,找到Downloads区域,这里有若干版本,这里我们选择了7.0的稳定版本 2、我们下载的是redis-7.0.15.tar.gz,这是一个压缩包,我们双击解压这个压缩包,可以得到如下文件 二、安装Redis 1、我们进入redis根目录安装mak…

Orange3数据可视化(树查看器-决策树)

树视图 分类和回归树的可视化。 输入 树&#xff1a;决策树 输出 选中的数据&#xff1a;从树节点中选中的实例 数据&#xff1a;带有额外一列&#xff0c;显示每个点是否被选中 这是一个多功能的小部件&#xff0c;用于展示分类和回归树的2D可视化。用户可以选择一个节点…

jvm知识点总结(二)

Java8默认使用的垃圾收集器是什么? Java8版本的Hotspot JVM,默认情况下使用的是并行垃圾收集器&#xff08;Parallel GC&#xff09; 如果CPU使用率飙升&#xff0c;如何排查? 1.先通过top定位到消耗最高的进程id 2.执行top -h pid单独监控该进程 3.在2中输入H&#xff…

Laravel 6 - 第十八章 模型

​ 文章目录 Laravel 6 - 第一章 简介 Laravel 6 - 第二章 项目搭建 Laravel 6 - 第三章 文件夹结构 Laravel 6 - 第四章 生命周期 Laravel 6 - 第五章 控制反转和依赖注入 Laravel 6 - 第六章 服务容器 Laravel 6 - 第七章 服务提供者 Laravel 6 - 第八章 门面 Laravel 6 - …

SN75107BDR 总线接收器 中文资料_PDF中文资料_参数_引脚图

SN75107BDR 规格信息&#xff1a; 制造商:Texas Instruments 产品种类:总线接收器 RoHS:是 接收机数量:2 Receiver 接收机信号类型:Differential 电源电压-最小:/- 4.75 V 电源电压-最大:/- 5.25 V 工作电源电流:30 mA 最小工作温度:0 C 最大工作温度: 70 C 封装 / 箱…

Honor of Kings PC Simulator S35

Honor of Kings PC Simulator S35 [王者荣耀PC模拟器S35] 1&#xff09;卡顿&#xff0c;延迟高 2&#xff09;技能方向控制麻烦 3&#xff09;技能释放位置麻烦 4&#xff09;方向控制麻烦 2024-04-26 04-00-16-Honor of Kings PC Simulator S35 [王者荣耀PC模拟器S35]_哔…

小白学习SpringCloud之Eureka

前言 需要搭建springcloud项目&#xff0c;eureka是其中的一个模块&#xff0c;依赖主要继承父依赖 学习视频&#xff1a;b站狂神说 便于理解,我修改了本地域名》这里!!! 127.0.0.1 eureka7001.com 127.0.0.1 eureka7002.com 127.0.0.1 eureka7003.comEureka入门案例 eureka…

Pytorch迁移学习训练病变分类模型

划分数据集 1.创建训练集文件夹和测试集文件夹 # 创建 train 文件夹 os.mkdir(os.path.join(dataset_path, train))# 创建 test 文件夹 os.mkdir(os.path.join(dataset_path, val))# 在 train 和 test 文件夹中创建各类别子文件夹 for Retinopathy in classes:os.mkdir(os.pa…