全栈从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,一经查实,立即删除!

相关文章

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 …

重生奇迹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;有的人则能善于从细小的数据着手发现问题。 我知道和了解的一些游戏…

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;用…

Transformer模型详解01-Word Embedding

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

【上岗认证】错题整理记录

目录 &#x1f31e;一、阶段1&#xff1a;编码规范 &#x1f30a;编码规范考试-CC &#x1f31e;二、阶段2&#xff1a;开发基础 &#x1f30a;C/C &#x1f30a;数据库&#xff08;Oracle/MySql&#xff09; &#x1f31e;三、阶段3&#xff1a;测试基础 &#x1f30a;…

Springboot+Vue项目-基于Java+MySQL的家政服务平台系统(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f49e;当前专栏&#xff1a;Java毕业设计 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f380; Python毕业设计 &…

实时数仓选型

实时数仓选型 实时数仓选型第一版实时数仓选型第二版 实时数仓选型第一版 实时数仓分层: 计算框架:Flink;存储框架:消息队列(可以实时读取&可以实时写入)ODS:Kafka 使用场景:每过来一条数据,读取到并加工处理DIM: HBase 使用场景:事实表会根据主键获取一行维表数据(1.永…

陪诊小程序开发:线上陪诊行业的发展

在人口老龄化的严重的当下&#xff0c;老人看病也更加困难&#xff0c;而陪诊行业作为一个新型行业&#xff0c;正在走入人们的生活中&#xff0c;帮助大众解决看病难等问题&#xff0c;为大众带来便捷高效的就医环境。 随着互联网时代的到来&#xff0c;各行各业也都开始向线…

Python+Selenium基于PO模式的Web自动化测试框架

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、什么是Selenium&#xff1f; Selenium是一个基于浏览器的自动化测试工具&#xff0c;它提供…