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

功能演示

演示视频

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.mzph.cn/web/4895.shtml

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

相关文章

Nginx自定义状态码499出现原因

499状态码定义 维基百科的定义 499 Client Closed Request (Nginx) Used in Nginx logs to indicate when the connection has been closed by client while the server is still processing itsrequest, making server unable to send a status code back 499状态码是nginx自…

Vue2 和 Vue3 的区别 (性能,编码方式,API 特性,源码)

在这个快节奏的时代&#xff0c;技术更新换代的速度也越来越快。我一直在使用Vue 2来开发项目。然而&#xff0c;现在越来越多的公司转向了Vue 3&#xff0c;并将其作为主要的前端开发框架。这就需要我们及时跟进新技术的发展&#xff0c;以保持竞争力。因此&#xff0c;我决定…

C语言阶段性测试错题纠正与拓展

引言&#xff1a;在2024年4月26日&#xff0c;我进行了C语言知识的“期末考试”。通过这次考试&#xff0c;我发现了我的知识漏洞。所以&#xff0c;我写下这篇博客来记录我的错题&#xff0c;并进行纠正&#xff0c;然后对于以前遗忘知识的回顾。 更多有关C语言的知识详解可前…

HarmonyOS编程实践系列:第一节 - 创建健康App欢迎页

系列文章目录 &#xff08;零&#xff09;鸿蒙HarmonyOS入门&#xff1a;如何配置环境&#xff0c;输出“Hello World“ &#xff08;一&#xff09;鸿蒙HarmonyOS开发基础 &#xff08;二&#xff09;鸿蒙HarmonyOS主力开发语言ArkTS-基本语法 &#xff08;三&#xff09;鸿蒙…

梳理一下低代码的真正价值!另推荐超好用的5款低代码开发平台

一、先来聊聊低代码的真实价值&#xff01; 在回答这个问题之前&#xff0c;我们不妨先来看两个案例&#xff1a; 某连锁商超企业在发展中产生了新的业务需求&#xff0c;一是希望能够快速展示门店销售数据&#xff0c;满足高层的管理需求&#xff1b;二是希望巡店、上架商品…

字典及GitHub字典爬取工具

红队API接口Fuzz字典可以用于WEB安全&#xff0c;渗透测试&#xff0c;SRC等场景 完整文件已上传知识星球&#xff0c;需要的朋友可加入查看。

Jitter 和相位噪声

近期学习PLL的时候&#xff0c;发现里面有诸多jitter类型&#xff0c;绕晕了&#xff0c;写个帖子记录一下学习过程。 目录 基本jitter和相噪概念 jitter 相位噪声 相位噪声和jitter之间的联系 三种常见的抖动类型及其特点 基本jitter和相噪概念 无噪声振荡器的输出是一…

状态模式

文章目录 1.UML类图2.状态基类3.状态实现类3.状态机管理类使用示例 1.UML类图 2.状态基类 public abstract class State {public string? Name { get; set; }public StateMachine? StateMachine {get; set;}public abstract void Exit();public abstract void Enter(); }3.…

生成删除数据库表数据的SQL语句

--获取表的名称及用表中第一列的注释作表的说明 select delete from ,Name2 ,--,RANK()OVER (ORDER BY Name2 DESC) as 序号,字段说明 from ( select bb.name as Name2,cc.* from ( select min(minor_id) as minor_id,id from ( SELECT g.minor_id minor_id,a.Name as Name …

解析vue.config.js文件

一、用途 创建 Vue 项目时&#xff0c;默认情况下是没有 vue.config.js 文件的。Vue CLI 会提供一组默认的配置&#xff0c;用于构建和开发项目&#xff0c;这些配置在内部被封装好了&#xff0c;并不需要用户手动创建 vue.config.js 文件来进行配置。通过在项目根目录下创建 …

重生奇迹MU首饰属性之迷

雷&#xff1a;抵抗移位、掌心雷 冰&#xff1a;抵抗冰度冻、冰封问箭、暴风雪、冰封 毒&#xff1a;降低中毒几率&#xff08;中毒不掉血&#xff09;、毒咒、毒炎 风&#xff1a;抵抗移位旋风斩&#xff08;没试过不过很多人用&#xff09;、龙卷风 至于火水地因为并没有…

分布式与一致性协议之Paxos算法(三)

Paxos算法 兰伯特关于Multi-Paxos的思考 领导者 我们可以通过引入领导者(Leader)节点来解决第一个问题。也就是说将领导者节点作为唯一提议者&#xff0c;如图所示。这样就不存在多个提议者同时提交提案的情况&#xff0c;也就不存在提案冲突的情况了。这里补充一点:在论文中…

NAT网络地址转换实验(思科)

华为设备参考&#xff1a;NAT网络地址转换实验&#xff08;华为&#xff09; 一&#xff0c;技术简介 NAT&#xff08;Network Address Translation&#xff09;&#xff0c;即网络地址转换技术&#xff0c;是一种在现代计算机网络中广泛应用的技术&#xff0c;主要用于有效管…

游戏新手村23:游戏数据分析都是谁在看数据

不管是做端游页游还是手游&#xff0c;不管是做市场广告投放还是游戏运营&#xff0c;都需要看数据。有的人说“数据会说话”&#xff0c;也有人说“数据会说谎”&#xff0c;有的人言必谈大数据&#xff0c;有的人则能善于从细小的数据着手发现问题。 我知道和了解的一些游戏…

react怎么只让接口请求一次

在React中&#xff0c;确保接口只请求一次通常涉及到组件的生命周期和状态管理。以下是一些常用的策略&#xff1a; 使用组件的useEffect钩子&#xff08;函数组件&#xff09;: 如果你使用的是函数组件&#xff0c;你可以使用useEffect钩子来发起请求&#xff0c;并确保它只在…

SpringBoot - java.lang.NoClassDefFoundError: XXX

问题描述 以 json-path 为例&#xff1a;java.lang.NoClassDefFoundError: com/jayway/jsonpath/Configuration 原因分析 编译不报错&#xff0c;但是运行时报错。 遇到这样类似的问题&#xff0c;首先就要想到是不是 Jar 包冲突引起的&#xff0c;或者引入的不是理想的 Jar…

数据仓库实验二:关联规则挖掘实验

目录 一、实验目的二、实验内容和要求三、实验步骤1、创建数据库和表2、挖掘关联规则&#xff08;1&#xff09;新建一个 Analysis Services 项目 Sales&#xff08;2&#xff09;建立数据源视图&#xff08;3&#xff09;建立挖掘结构 Sales.dmm&#xff08;4&#xff09;部署…

Java集合相关的List、Set、Map基础知识

目录 一、集合介绍 二、List 三、Map HashMap的数据结构 如何理解红黑树 四、set 一、集合介绍 在Java中&#xff0c;集合是一种用于存储对象的数据结构&#xff0c;它提供了一种更加灵活和强大的方式来处理和操作数据。Java集合框架提供了一系列接口和类&#xff0c;用…

Pointnet++改进即插即用系列:全网首发PPA反向残差移动块 |即插即用,提升特征提取模块性能

简介:1.该教程提供大量的首发改进的方式,降低上手难度,多种结构改进,助力寻找创新点!2.本篇文章对Pointnet++特征提取模块进行改进,加入PPA,提升性能。3.专栏持续更新,紧随最新的研究内容。 目录 1.理论介绍 2.修改步骤 2.1 步骤一 2.2 步骤二 2.3 步骤三

Transformer模型详解01-Word Embedding

文章目录 前言Transformer 整体结构Transformer 的输入单词 Embedding原理CBOW 模型one-hot构建 CBOW 训练数据集构建 CBOW 神经网络训练 CBOW 神经网络 Skip-gram 模型one-hot构建 Skip-gram训练数据集训练 Skip-gram神经网络 Word2Vec实例数据训练保存和加载 前言 Transform…