add drumming

This commit is contained in:
LeeJu 2026-05-22 16:31:31 +08:00
parent faffa6ed96
commit c02e0a69a9
68 changed files with 2888 additions and 0 deletions

21
drumming/README.md Normal file
View File

@ -0,0 +1,21 @@
# pointgaming
### 1、项目说明
总体思路数独程序项目开发的总体思路如图1.1示意,拟实现头部区域,主体区域,右边的内容展示和操作区域等。
图1.1 项目开发的总体思路图
<img src="figs/overall.png" width=80% /></center>
### 2、开发环境说明
1系统版本OpenHarmony 4.0 Release
2SDK版本API 10
3Model类型Stage
4DevEco Studio版本Deveco Studio 4.0 release
5硬件环境Unionpi Whale、RK3566开发板
6预览器参数(1)DeviceType:default;(2)Resolution:560-1280;(3)DPI:240;其余参数自定。
### 3、项目主要函数调用关系图与项目输出界面演进图
图3.1 项目主要函数调用关系图
<img src="figs/relation.png" width=80% /></center>
图3.2 项目输出界面演进图
<img src="figs/evolution.png" width=80% /></center>

11
drumming/drummer/.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/node_modules
/oh_modules
/local.properties
/.idea
**/build
/.hvigor
.cxx
/.clangd
/.clang-format
/.clang-tidy
**/.test

View File

@ -0,0 +1,10 @@
{
"app": {
"bundleName": "com.test.myapplication",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}

View File

@ -0,0 +1,8 @@
{
"string": [
{
"name": "app_name",
"value": "drawpart3"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,49 @@
{
"app": {
"signingConfigs": [
{
"name": "default",
"material": {
"certpath": "C:\\Users\\cyf20\\.ohos\\config\\openharmony\\auto_ohos_default_drawpart3_com.test.myapplication.cer",
"storePassword": "0000001B112EEEE42E9C65C70B8BE139E9746FD3A593BEC334955CFDC40C11FEB89314E23E845E58F31D6E",
"keyAlias": "debugKey",
"keyPassword": "0000001BACB92E1829CB96E1D19A1D0FD4AF75EC50D07DAACF62C3FB0B8D5EF8E82E944DC0FD8F397E8901",
"profile": "C:\\Users\\cyf20\\.ohos\\config\\openharmony\\auto_ohos_default_drawpart3_com.test.myapplication.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "C:\\Users\\cyf20\\.ohos\\config\\openharmony\\auto_ohos_default_drawpart3_com.test.myapplication.p12"
}
}
],
"products": [
{
"name": "default",
"signingConfig": "default",
"compileSdkVersion": 10,
"compatibleSdkVersion": 10,
"runtimeOS": "OpenHarmony",
}
],
"buildModeSet": [
{
"name": "debug",
},
{
"name": "release"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": [
"default"
]
}
]
}
]
}

6
drumming/drummer/entry/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test

View File

@ -0,0 +1,31 @@
{
"apiType": "stageMode",
"buildOption": {
"arkOptions": {
// "apPath": "./modules.ap" /* Profile used for profile-guided optimization (PGO), a compiler optimization technique to improve app runtime performance. */
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": true,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}

View File

@ -0,0 +1,6 @@
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
}

View File

@ -0,0 +1,18 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope

View File

@ -0,0 +1,10 @@
{
"license": "",
"devDependencies": {},
"author": "",
"name": "entry",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {}
}

View File

@ -0,0 +1,38 @@
/**
* 字体大小(大)
*/
export const BIG_SIZE_FONT = 20
/**
* 字体大小(小)
*/
export const SMALL_SIZE_FONT = 15
//以da yu200屏幕为标准 imageShow组件的左上角和右下角的坐标
//经过测试在whale RK3588上影响不大
export const TOP_LEFT_X = 0
export const TOP_LEFT_Y = 38
export const BOTTOM_RIGHT_X = 681
export const BOTTOM_RIGHT_Y = 373
//定义绘制节点的大小vp
export const DRAW_NODE_SIZE = 40
let setting:RenderingContextSettings = new RenderingContextSettings(true)
//绘制未选择/一般情况
export const NORMAL_CONTEXT: CanvasRenderingContext2D = new CanvasRenderingContext2D(setting)
//绘制选中
export const SELECTED_CONTEXT: CanvasRenderingContext2D = new CanvasRenderingContext2D(setting)
export const X_CENTER = (BOTTOM_RIGHT_X - TOP_LEFT_X) / 2 - TOP_LEFT_X
export const Y_CENTER = (BOTTOM_RIGHT_Y - TOP_LEFT_Y) / 2 - TOP_LEFT_Y
//TODO KEY 用户需要自行更改
export const KEY = "14a9551d9cdbf6e2ce12768de807a5fe"
/**
* 程序会默认使用这些预定的城市作为参数调用API以获取数据 然后将其写入文件
*/
//模拟的城市
export const CITY = ["广州市","深圳市","珠海市","汕头市","佛山市","韶关市","湛江市","肇庆市","江门市","茂名市","惠州市","梅州市","汕尾市"]
//,"汕头市","佛山市","韶关市","湛江市","肇庆市","江门市","茂名市","惠州市","梅州市","汕尾市"

View File

@ -0,0 +1,56 @@
import { cp } from '../../utils/algorithm/customPath'
import { showToast } from '../../utils/functions/showToast'
import Node from '../dataStruct/class/Node'
@CustomDialog
export struct setNodeCount{
controller:CustomDialogController
@State count:number = 0
@Link Nodes:Array<Node>
@Consume refresh:boolean
@Consume isStartToPath:boolean
@Consume nodePathId:Array<number>
build(){
Column({space:5}){
Row(){
Text('设置个数:')
TextInput()
.layoutWeight(1)
.onChange((value)=>{
this.count = Number(value)
})
}.width('100%')
Row(){
Button('确定')
.type(ButtonType.Normal)
.onClick(()=>{
this.isStartToPath = false
this.nodePathId = []
if(this.count <= 1 || this.count >= 14){
showToast("节点数只能位于2至13之间")
return
}
this.Nodes = cp.createNodeData(this.count)
this.refresh = !this.refresh
this.controller.close()
})
Button('取消')
.type(ButtonType.Normal)
.onClick(()=>{
this.controller.close()
})
}.width('100%').justifyContent(FlexAlign.SpaceAround)
}.width('100%').padding(5)
}
}

View File

@ -0,0 +1,33 @@
import Node from '../dataStruct/class/Node'
@CustomDialog
export struct setNodeNameDialog{
controller:CustomDialogController
@ObjectLink node:Node
@State inputName:string = ""
build(){
Column({space:5}){
Row(){
Text('设置名称:')
TextInput()
.layoutWeight(1)
.onChange((value)=>{
this.inputName = value
})
}.width('100%')
Row(){
Button('确定')
.type(ButtonType.Normal)
.onClick(()=>{
this.node.label = this.inputName
this.controller.close()
})
Button('取消')
.type(ButtonType.Normal)
.onClick(()=>{
this.controller.close()
})
}.width('100%').justifyContent(FlexAlign.SpaceAround)
}.width('100%').padding(5)
}
}

View File

@ -0,0 +1,16 @@
import { RouteData } from './RouteData'
export class Data{
status:string
info:string
count:string
route:RouteData
constructor(status: string, info: string, count: string, route: RouteData) {
this.status = status
this.info = info
this.count = count
this.route = route
}
}

View File

@ -0,0 +1,21 @@
export default class Edge {
id: number; // “连接对象编号”:连接线唯一标识,通常是自然数编号
node1: number; // “节点对象1编号”
node2: number; // “节点对象2编号”连接线两端节点的编号
visible: boolean; // “连接是否显示”:表示该连接是否存在
marked: boolean; // ”连接是否标记“:该连接是否被标记
label: string; // ”连接标签“:该连接的名字
distance: number ; // ”连接的距离/成本“
time: number; // ”连接的时间“
constructor(id: number, node1: number, node2: number, visible: boolean, marked: boolean, label: string, distance: number, time: number) {
this.id = id;
this.node1 = node1;
this.node2 = node2;
this.visible = visible;
this.marked = marked;
this.label = label;
this.distance = distance;
this.time = time;
}
}

View File

@ -0,0 +1,16 @@
@Observed
export default class Node {
id: number; // “节点编号”:唯一标识一个节点,通常是自然数编号
label: string; // “节点标签”:节点名称(如城市名)
marked: boolean; // “节点是否标记”:节点是否被标记
coordinates: [number, number]; // “节点坐标”:节点坐标组<x, y>
mustVisit: boolean; // “是否为必经节点”:表示在遍历时是否必经该节点,默认为必经节点
constructor(id: number, label: string, marked: boolean, coordinates: [number, number], mustVisit: boolean = true) {
this.id = id;
this.label = label;
this.marked = marked;
this.coordinates = coordinates;
this.mustVisit = mustVisit;
}
}

View File

@ -0,0 +1,28 @@
import { StepData } from './StepData'
export class PathData{
path:string
distance:string
duration:string
strategy:string
tolls:string
restriction:string
traffic_lights:string
toll_distance:string
steps:Array<StepData>
constructor(path: string, distance: string, duration: string, strategy: string, tolls: string, restriction: string,
traffic_lights: string, toll_distance: string, steps: Array<StepData>) {
this.path = path
this.distance = distance
this.duration = duration
this.strategy = strategy
this.tolls = tolls
this.restriction = restriction
this.traffic_lights = traffic_lights
this.toll_distance = toll_distance
this.steps = steps
}
}

View File

@ -0,0 +1,16 @@
import { PathData } from './PathData'
export class RouteData{
origin:string
destination:string
taxi_cost:string
paths:Array<PathData>
constructor(origin: string, destination: string, taxi_cost: string, paths: Array<PathData>) {
this.origin = origin
this.destination = destination
this.taxi_cost = taxi_cost
this.paths = paths
}
}

View File

@ -0,0 +1,8 @@
export class StepData{
step:string
constructor(step: string) {
this.step = step
}
}

View File

@ -0,0 +1,6 @@
interface urbanTransitionCost {
distance: number,
time: number
}
export type CityInfo = Record<string, Record<string, urbanTransitionCost>>;

View File

@ -0,0 +1,6 @@
export interface nodeInfo {
to: number,
weight: number
}
export type Graph = Record<number, nodeInfo[]>;

View File

@ -0,0 +1,6 @@
export interface solutionData{
count?:number
time:string
path:string
otherPath:Array<string>
}

View File

@ -0,0 +1,194 @@
import { SMALL_SIZE_FONT } from '../common/CommonConstant'
import { setNodeCount } from '../common/customDialog/SetNodeCount'
import Node from '../common/dataStruct/class/Node'
import { solutionData } from '../common/dataStruct/interface/SolutionData'
import { cp } from '../utils/algorithm/customPath'
import { tsp } from '../utils/algorithm/TSP'
import { showToast } from '../utils/functions/showToast'
@Component
export struct header {
@Link resTime:number
@Link resDistance:number
@Prop headerContent:Array<string>
@Link curHeaderContentIndex:number
@Consume Nodes:Array<Node>
@Consume refresh: boolean;
@Consume curNodesIndex:number
@Consume isStartToPath:boolean
@Consume solutionData:solutionData
controller:CustomDialogController = new CustomDialogController({
builder:setNodeCount({Nodes:$Nodes})
})
@Builder operatorMenu(){
Menu(){
MenuItem({content:"随机生成一个TSP问题"})
.onClick(()=>{
tsp.createRandomTspProblem();
this.Nodes = cp.nodes;
this.curHeaderContentIndex = 0
this.isStartToPath = false
})
MenuItem({content:"重新更改节点位置"})
.onClick(()=>{
cp.regenerate();
this.refresh = !this.refresh;
this.curHeaderContentIndex = 0
this.isStartToPath = false
})
MenuItem({content:"自动求解最优路径-遍历"})
.onClick(()=>{
if (this.Nodes.length <= 10) {
let startNodeId = this.Nodes[this.curNodesIndex].id;
// let result: [number[][], number, number, number] = cp.solveTspInTraversal(startNodeId);
let result: [number[][], number, number, number] = cp.solveTsp(startNodeId, cp.traversalSearchToPath);
let NodeIdsPaths: number[][] = result[0].slice();
if (!NodeIdsPaths.length) return;
let displayPath: number[] = NodeIdsPaths[0]; // 要展示的最短路径
let enumPaths: number[][] = NodeIdsPaths.slice(1); // 其余的最短路径
cp.setDisplayPath(displayPath);
this.refresh = !this.refresh;
let distanceCost: number = result[1]; // 距离成本
let runtime: number = result[2];
let runtimeTag: string = ""; // 运行时间的字符串表示,已有单位
if (runtime == 0) {
runtimeTag = "<0.001s";
} else {
runtimeTag = runtime.toFixed(3) + "s";
}
let traversalNum: number = result[3]; // 遍历次数
cp.createGraph(this.Nodes, false);
let timeCost: number = cp.calculatePathCost(cp.graph, displayPath); // 时间成本
this.curHeaderContentIndex = 3
this.resTime = Number((timeCost / 60).toFixed(0))
this.resDistance = Number((distanceCost * 1e-3).toFixed(3))
this.solutionData.count = traversalNum
this.solutionData.time = runtimeTag
this.solutionData.path = ""
this.solutionData.otherPath = [""]
for (let index = 0; index < displayPath.length; index++) {
this.solutionData.path += (this.Nodes[displayPath[index]].label)
if (index != displayPath.length - 1) {
this.solutionData.path += "->";
}
}
for (let index = 0; index < enumPaths.length; index++) {
for (let i = 0; i < enumPaths[index].length; i++) {
this.solutionData.otherPath[index] += (this.Nodes[enumPaths[index][i]].label)
if (i != enumPaths[index].length - 1) {
this.solutionData.otherPath[index] += "->";
}
}
}
} else {
showToast("超过11个节点的遍历法求tsp问题会导致程序崩溃不建议使用");
}
})
MenuItem({content:"自动求解优化路径-贪心"})
.onClick(()=>{
let startNodeId = this.Nodes[this.curNodesIndex].id;
let result: [number[][], number, number, number] = cp.solveTsp(startNodeId, cp.greedySearchToPath);
if (!result[0].length) return;
let displayPath: number[] = result[0][0].slice(); // 要展示的最短路径
cp.setDisplayPath(displayPath);
this.refresh = !this.refresh;
let distanceCost: number = result[1]; // 距离成本
let runtime: number = result[2];
let runtimeTag: string = ""; // 运行时间的字符串表示,已有单位
if (runtime == 0) {
runtimeTag = "<0.001s";
} else {
runtimeTag = runtime.toFixed(3) + "s";
}
cp.createGraph(this.Nodes, false);
let timeCost: number = cp.calculatePathCost(cp.graph, displayPath); // 时间成本
this.curHeaderContentIndex = 4
this.resTime = Number((timeCost / 60).toFixed(0))
this.resDistance = Number((distanceCost * 1e-3).toFixed(3))
this.solutionData.time = runtimeTag
this.solutionData.path = ""
for (let index = 0; index < displayPath.length; index++) {
this.solutionData.path += (this.Nodes[displayPath[index]].label)
if (index != displayPath.length - 1) {
this.solutionData.path += "->";
}
}
})
MenuItem({content:"检查是否存在路径"})
.onClick(()=>{
let id1:number|undefined = AppStorage.get("startNodeId")
let id2:number|undefined = AppStorage.get("endNodeId")
if(id1 == undefined || id2 == undefined){
showToast("未选择两个节点,请前往路径操作中去选择两个节点")
return
}
let node1Id: number = id1;
let node2Id: number = id2;
if (cp.graphCheckConnectivity(this.Nodes[node1Id], this.Nodes[node2Id])[0]) {
showToast(`节点${node1Id} (${this.Nodes[node1Id].label})和节点${node2Id} (${this.Nodes[node2Id].label})之间存在路径`);
} else {
showToast(`节点${node1Id} (${this.Nodes[node1Id].label})和节点${node2Id} (${this.Nodes[node2Id].label})之间不存在路径`);
}
})
}.radius(0)
}
build() {
Row({space:5}){
ForEach(this.headerContent,(item:string,index:number)=>{
if(index <= 2){
Text(item)
.fontSize(SMALL_SIZE_FONT)
.fontColor(this.curHeaderContentIndex == index?Color.Red:Color.Black)
.onClick(()=>{
this.curHeaderContentIndex = index
if(this.curHeaderContentIndex != 2){
this.isStartToPath = false
}
})
}
})
Blank()
Row({space:5}){
Text("节点数目 " + (this.Nodes.length))
.fontSize(SMALL_SIZE_FONT)
Button('修改')
.width(60).height(25)
.type(ButtonType.Normal)
.onClick(()=>{
this.controller.open()
})
}
Text("时间 " + this.resTime + " 分钟")
.fontSize(SMALL_SIZE_FONT)
Text("距离(成本) " + this.resDistance+ " 千米")
.fontSize(SMALL_SIZE_FONT)
Blank()
Text("TSP问题生成与自动求解")
.onClick(()=>{
this.isStartToPath = false
})
.fontSize(SMALL_SIZE_FONT)
.bindMenu(this.operatorMenu)
}.width('100%').height('100%').padding(5).backgroundColor('#F8F8F8')
}
}

View File

@ -0,0 +1,96 @@
import { DRAW_NODE_SIZE, NORMAL_CONTEXT, SELECTED_CONTEXT } from '../common/CommonConstant'
import Edge from '../common/dataStruct/class/Edge'
import Node from '../common/dataStruct/class/Node'
import { drawEdgeList } from '../utils/functions/drawEdge'
import { drawNode, drawNodeList } from '../utils/functions/drawNode'
import { showToast } from '../utils/functions/showToast'
import { Logger } from '../utils/Logger'
@Component
export struct imageShow {
@Consume Nodes:Array<Node>
@Consume curNodesIndex:number
@Consume Edges:Array<Edge>
@Consume refresh:boolean
@Consume isStartToPath:boolean
@Consume nodePathId:Array<number>
@Link curEdgesIndex:number
@Prop loop:string = "100%"
build() {
Stack() {
Canvas(SELECTED_CONTEXT).width('100%').height(this.loop)
.onReady(()=>{
SELECTED_CONTEXT.beginPath()
SELECTED_CONTEXT.strokeStyle = 'yellow'
SELECTED_CONTEXT.font = '20vp'
drawNode(this.Nodes[this.curNodesIndex],SELECTED_CONTEXT)
})
Canvas(NORMAL_CONTEXT).width('100%').height(this.loop)
.onReady(()=>{
NORMAL_CONTEXT.beginPath()
NORMAL_CONTEXT.strokeStyle = 'green'
NORMAL_CONTEXT.fillStyle = 'green'
//绘制线
drawEdgeList(this.Edges,false)
// drawEdgeList(this.Edges,true)
NORMAL_CONTEXT.font = '15vp'
//绘制点
drawNodeList(this.Nodes.slice(0,this.curNodesIndex),NORMAL_CONTEXT)
if(this.curNodesIndex == this.Nodes.length - 1){
return
}
drawNodeList(this.Nodes.slice(this.curNodesIndex + 1,this.Nodes.length),NORMAL_CONTEXT)
})
.onClick((event:ClickEvent)=>{
for (let index = 0; index < this.Nodes.length; index++) {
//是否点到节点上
if(event.x >= this.Nodes[index].coordinates[0] && event.x <= this.Nodes[index].coordinates[0] + DRAW_NODE_SIZE &&
event.y >= this.Nodes[index].coordinates[1] && event.y <= this.Nodes[index].coordinates[1] + DRAW_NODE_SIZE){
//进入到标记模式
if (this.isStartToPath) {
let i = index
if(this.Nodes[i].marked){
showToast("不允许选择相同的节点")
}else{
this.curNodesIndex = index
this.Nodes[this.curNodesIndex].marked = true
this.nodePathId.push(index)
}
}else{
this.curNodesIndex = index
this.refresh = !this.refresh
}
return
}
}
for (let index = 0; index < this.Edges.length; index++) {
if(!this.Edges[index].visible){
continue
}
let x1 = this.Nodes[this.Edges[index].node1].coordinates[0] + DRAW_NODE_SIZE / 2
let y1 = this.Nodes[this.Edges[index].node1].coordinates[1] + DRAW_NODE_SIZE / 2
let x2 = this.Nodes[this.Edges[index].node2].coordinates[0] + DRAW_NODE_SIZE / 2
let y2 = this.Nodes[this.Edges[index].node2].coordinates[1] + DRAW_NODE_SIZE / 2
let A = y2 - y1
let B = x1 - x2
let C = x2*y1 - x1*y2
let t1 = A * event.x + B * event.y + C
if(t1 < 0){
t1 = -t1
}
let t2 = Math.sqrt((A * A + B * B))
let d = t1 / t2
Logger.i("index" + index + "label" + this.Nodes[this.Edges[this.curEdgesIndex].node1].label + this.Nodes[this.Edges[this.curEdgesIndex].node2].label )
if(d < 3){
this.curEdgesIndex = index
Logger.i("cur - index" + index)
return
}
}
})
}
.width('100%').height('100%')
}
}

View File

@ -0,0 +1,460 @@
import { BIG_SIZE_FONT , SMALL_SIZE_FONT } from '../common/CommonConstant'
import { setNodeNameDialog } from '../common/customDialog/SetNodeNameDialog'
import Edge from '../common/dataStruct/class/Edge'
import Node from '../common/dataStruct/class/Node'
import { solutionData } from '../common/dataStruct/interface/SolutionData'
import { cp } from '../utils/algorithm/customPath'
import { showToast } from '../utils/functions/showToast'
import { Logger } from '../utils/Logger'
@Component
export struct operateBar {
@Prop headerContent:Array<string>
@Link curHeaderContentIndex:number
@Consume Nodes:Array<Node>
@Consume curNodesIndex:number
build() {
Column(){
Text(this.headerContent[this.curHeaderContentIndex])
.fontSize(BIG_SIZE_FONT)
if(this.curHeaderContentIndex == 0){
nodeOP({selectNode:this.Nodes[this.curNodesIndex]})
.width('100%').layoutWeight(1)
}
if(this.curHeaderContentIndex == 1){
linkOP()
.width('100%').layoutWeight(1)
}
if(this.curHeaderContentIndex == 2){
pathOP()
.width('100%').layoutWeight(1)
}
if(this.curHeaderContentIndex == 3){
solution_traverse()
}
if(this.curHeaderContentIndex == 4){
solution_greedy()
}
}.width('100%').height('100%').backgroundColor(Color.Pink)
}
}
@Component
struct nodeOP {
@Consume Nodes:Array<Node>
@Consume curNodesIndex:number
@Consume refresh:boolean
@ObjectLink selectNode:Node
controller:CustomDialogController = new CustomDialogController({
builder:setNodeNameDialog({node:this.Nodes[this.curNodesIndex]})
})
build() {
Column({space:5}){
Text('当前共有' + this.Nodes.length + "个节点")
.fontSize(SMALL_SIZE_FONT)
Row({space:5}){
Text('当前节点ID:')
.fontSize(SMALL_SIZE_FONT)
Text(this.selectNode.id.toString())
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
Row({space:5}){
Text('当前节点标签:')
.fontSize(SMALL_SIZE_FONT)
Text(this.selectNode.label)
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
Button('设置标签')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(()=>{
this.controller.open()
})
Button('删除当前节点')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(()=>{
if (this.Nodes.length <= 2){
showToast("节点数不可少于2")
return
}
this.Nodes = cp.deleteNodeData(this.curNodesIndex)
})
Button('设置为非经过点')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(()=>{
let c = 0
for (let index = 0; index < this.Nodes.length; index++) {
if(!this.Nodes[index].mustVisit){
c++;
}
if(c == 2){
showToast("最多只能选择两个非经过点")
return
}
}
this.Nodes[this.curNodesIndex].mustVisit = false
this.refresh = !this.refresh
})
Button('设置为经过点')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(()=>{
this.Nodes[this.curNodesIndex].mustVisit = true
this.refresh = !this.refresh
})
Button('增加一个节点')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(()=>{
if (this.Nodes.length >= 13) {
showToast("节点数只能位于2至13之间")
return
}
this.Nodes = cp.addNodeData()
})
}.width('100%').height('100%')
}
}
@Component
struct linkOP {
@Consume Nodes:Array<Node>
@Consume curNodesIndex:number
@Consume Edges:Array<Edge>
@Consume curEdgesIndex:number
@State isOnClick:boolean = false
@State startNode:Node|undefined = undefined
@State endNode:Node|undefined = undefined
@Consume refresh:boolean
build() {
Scroll() {
Column({ space: 5 }) {
Button('建立为完全图')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(() => {
for (let index = 0; index < this.Edges.length; index++) {
this.Edges[index].visible = true
}
this.refresh = !this.refresh
})
Button('删除所有连接')
.type(ButtonType.Normal)
.fontSize(SMALL_SIZE_FONT)
.onClick(() => {
for (let index = 0; index < this.Edges.length; index++) {
this.Edges[index].visible = false
}
this.refresh = !this.refresh
})
Column({ space: 5 }) {
Text('当前选中的连接:')
.fontSize(SMALL_SIZE_FONT)
Text(`从 节点${this.Edges[this.curEdgesIndex].node1}:` + this.Nodes[this.Edges[this.curEdgesIndex].node1].label)
.fontSize(SMALL_SIZE_FONT)
Text(`到 节点${this.Edges[this.curEdgesIndex].node2}:` + this.Nodes[this.Edges[this.curEdgesIndex].node2].label)
.fontSize(SMALL_SIZE_FONT)
Text(`距离${(this.Edges[this.curEdgesIndex].distance * 1e-3).toFixed(3)}千米`)
.fontSize(SMALL_SIZE_FONT)
Text(`时间${(this.Edges[this.curEdgesIndex].time / 60).toFixed(0)}分钟`)
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
Button(this.isOnClick ? "取消" : '选择两个节点进行连接')
.type(ButtonType.Normal)
.onClick(() => {
this.startNode = this.Nodes[this.curNodesIndex]
this.isOnClick = !this.isOnClick
})
Row({ space: 5 }) {
Text('当前节点:')
.fontSize(SMALL_SIZE_FONT)
Text(this.isOnClick ? this.startNode!.label : this.Nodes[this.curNodesIndex].label)
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
if (this.isOnClick) {
Row({ space: 5 }) {
Text('要连接节点:')
.fontSize(SMALL_SIZE_FONT)
Text(this.Nodes[this.curNodesIndex].label)
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
Button('确定')
.type(ButtonType.Normal)
.onClick(() => {
let startNodeId = this.startNode!.id
let endNodeId = this.Nodes[this.curNodesIndex].id
Logger.i(startNodeId + " " + endNodeId)
this.startNode = this.Nodes[this.curNodesIndex]
if (startNodeId == endNodeId) {
showToast("不允许连接同一个节点")
return
}
this.isOnClick = false
cp.connectTwoNodes(startNodeId, endNodeId);
this.refresh = !this.refresh;
})
}
}
}.width('100%').height('100%').padding({bottom:5})
}
}
@Component
struct pathOP {
@Consume Nodes:Array<Node>
@Consume curNodesIndex:number
@Consume Edges:Array<Edge>
@Consume refresh:boolean
@Consume nodePathId:Array<number>
@Consume nodePath:string
@Consume isStartToPath:boolean
@Consume solutionData:solutionData
@State isSelectNode:boolean = false
@State selectIndex:number = 0
@State index:number = -1
@State startNode:Node|undefined = undefined
@State endNode:Node|undefined = undefined
@State displayShortPath: boolean = false
build() {
Scroll(){
Column({space:5}){
Button(this.isStartToPath?"结束标记":'标记路径')
.type(ButtonType.Normal)
.onClick(()=>{
if(this.isSelectNode){
return
}
if(!this.isStartToPath){
this.nodePathId = []
} else {
if (!cp.checkPathLegitimate(this.nodePathId)) {
this.nodePathId.length = 0;
cp.clearPath();
}
}
this.isStartToPath = !this.isStartToPath
//先置空
for (let index = 0; index < this.Edges.length; index++) {
this.Edges[index].marked = false
}
for (let index = 0; index < this.Nodes.length; index++) {
this.Nodes[index].marked = false
}
this.refresh = !this.refresh
cp.setDisplayPath(this.nodePathId);
})
Text('用户指定路径:' + this.nodePath)
.fontSize(SMALL_SIZE_FONT)
.width("100%")
if(this.isStartToPath){
Row(){
Button('清除')
.type(ButtonType.Normal)
.onClick(()=>{
this.nodePathId = []
cp.clearPath();
for (let index = 0; index < this.Nodes.length; index++) {
this.Nodes[index].marked = false
}
})
Button('撤销')
.type(ButtonType.Normal)
.onClick(()=>{
cp.undoPath();
if(this.nodePathId.length == 0){
return
}
this.Nodes[this.nodePathId[this.nodePathId.length-1]].marked = false
this.nodePathId.pop()
return
})
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
Button('检查路径是否合法')
.type(ButtonType.Normal)
.onClick(()=>{
cp.checkPathLegitimate(this.nodePathId);
})
}
Button('判断是否为最短路径')
.type(ButtonType.Normal)
.onClick(()=>{
if(this.isStartToPath){
return
}
if(this.isSelectNode){
return
}
if(!this.nodePath.length){
return
}
if(this.Nodes.length <= 10){
if (cp.checkUserBest(cp.path, this.nodePathId[0])) {
showToast("当前路径是最短路径");
} else {
showToast("当前路径不是最短路径");
}
}else{
showToast("超过11个节点的遍历法求tsp问题会导致程序崩溃不建议使用");
}
})
Button('判断是否为贪心路径')
.type(ButtonType.Normal)
.onClick(()=>{
if(this.isStartToPath){
return
}
if(this.isSelectNode){
return
}
if(!this.nodePath.length){
return
}
if (cp.checkIsGreedy(cp.path, this.nodePathId[0])) {
showToast("当前路径是贪心路径");
} else {
setTimeout(() => {
showToast("当前路径不是贪心路径");
}, 2000);
}
})
Row(){
Button(this.isSelectNode?"取消":'选择两个节点')
.type(ButtonType.Normal)
.onClick(()=>{
if(this.isStartToPath){
return
}
this.isSelectNode = !this.isSelectNode
this.selectIndex = this.curNodesIndex
this.startNode = this.Nodes[this.curNodesIndex]
this.endNode = this.Nodes[this.curNodesIndex]
this.index = -1
this.displayShortPath = false;
this.solutionData.path = "";
this.solutionData.count = 0;
this.Nodes.forEach(node => {
node.mustVisit = true;
})
this.refresh = !this.refresh;
})
if(this.isSelectNode){
Button("确定")
.type(ButtonType.Normal)
.onClick(()=>{
if(this.curNodesIndex == this.startNode!.id){
showToast("不能选择相同的节点")
return
}
if (!cp.graphCheckConnectivity(this.Nodes[this.curNodesIndex], this.Nodes[this.startNode!.id])[0]) {
showToast("节点间不可达,请重新选择节点");
return;
}
if(this.index != -1){
return
}
this.endNode = this.Nodes[this.curNodesIndex]
this.index = this.curNodesIndex
AppStorage.setOrCreate("startNodeId",this.startNode!.id)
AppStorage.setOrCreate("endNodeId",this.endNode!.id)
})
}
}
Text(`当前节点1ID:${this.isSelectNode?this.selectIndex + `(${this.Nodes[this.selectIndex].label})`:this.curNodesIndex + `(${this.Nodes[this.curNodesIndex].label})`}`)
.fontSize(SMALL_SIZE_FONT)
if(this.isSelectNode){
Text(`当前节点2ID:${this.index != -1?this.index + `(${this.Nodes[this.index].label})`:this.curNodesIndex + `(${this.Nodes[this.curNodesIndex].label})`}`)
.fontSize(SMALL_SIZE_FONT)
Button('求解最短路径')
.type(ButtonType.Normal)
.onClick(()=>{
this.displayShortPath = false;
this.solutionData.path = "";
this.solutionData.count = 0;
if(this.endNode != undefined){
if(this.endNode!.id != this.startNode!.id){
let shortPathInfo: [number[], number] = cp.shortestPathAlgorithm(this.startNode!.id, this.endNode!.id);
let shortPath: number[] = shortPathInfo[0];
this.solutionData.count = Number((shortPathInfo[1] * 1e-3).toFixed(3));
for (let index = 0; index < shortPath.length; index++) {
this.solutionData.path += (this.Nodes[shortPath[index]].label)
if (index != shortPath.length - 1) {
this.solutionData.path += "->";
}
}
if (this.solutionData.path != "") {
this.displayShortPath = true;
} else {
showToast("没有找到路径");
}
return;
}
}
showToast("请选择第二个节点,或点击确定")
})
if (this.displayShortPath) {
Text('最短路径长度:' + this.solutionData.count + "km")
.fontSize(SMALL_SIZE_FONT)
.width("100%")
Text('最短路径为:' + this.solutionData.path)
.fontSize(SMALL_SIZE_FONT)
.width("100%")
}
}
}
}.width('100%').height('100%').padding({bottom:5})
}
}
@Component
struct solution_traverse {
@Consume solutionData:solutionData
build() {
Column({space:5}){
Text('遍历次数' + this.solutionData.count!)
.fontSize(SMALL_SIZE_FONT)
Text('求解花费时间' + this.solutionData.time)
.fontSize(SMALL_SIZE_FONT)
Text('最短路径:' + this.solutionData.path)
.fontSize(SMALL_SIZE_FONT)
.width("100%")
ForEach(this.solutionData.otherPath,(item:string,index:number)=>{
Row(){
Text('其他最短路径' + (index + 1) + ": " + item)
.fontSize(SMALL_SIZE_FONT)
}.width('100%')
})
}.width('100%').height('100%')
}
}
@Component
struct solution_greedy{
@Consume solutionData:solutionData
build() {
Column({space:5}){
Text('求解花费时间' + this.solutionData.time)
.fontSize(SMALL_SIZE_FONT)
Text('最短路径:' + this.solutionData.path)
.fontSize(SMALL_SIZE_FONT)
.width('100%')
}.width('100%').height('100%')
}
}

View File

@ -0,0 +1,60 @@
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import window from '@ohos.window';
import dataPreferences from '@ohos.data.preferences';
export default class EntryAbility extends UIAbility {
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
let dp = await dataPreferences.getPreferences(this.context,"web")
let loop = await dp.has("isLoad")
if(loop){
//读取文件操作
AppStorage.setOrCreate('isOpFile',true)
}else{
dp.put("isLoad",true,()=>{
dp.flush()
})
//调用api操作
AppStorage.setOrCreate('isOpFile',false)
}
}
onDestroy(): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
}
}

View File

@ -0,0 +1,126 @@
import { CityInfo } from '../common/dataStruct/interface/CityInfo';
import { X_CENTER, Y_CENTER } from '../common/CommonConstant';
import Edge from '../common/dataStruct/class/Edge';
import Node from '../common/dataStruct/class/Node';
import { solutionData } from '../common/dataStruct/interface/SolutionData';
import { header } from '../component/Header';
import { imageShow } from '../component/ImageShow';
import { operateBar } from '../component/OperateBar';
import { cp } from '../utils/algorithm/customPath';
import { getRealData } from '../utils/functions/getRealData';
import { Logger } from '../utils/Logger';
import { createCityInfo } from '../utils/functions/createCityInfo';
export let cityInfo: CityInfo = {};
@Entry
@Component
struct Index {
@State headerContent:Array<string> = ["节点操作","连接操作","路径操作","最短路径-遍历","最短路径-贪心"]
@State @Watch('refreshOption')curHeaderContentIndex:number = 0
refreshOption(){
for (let index = 0; index < this.Nodes.length; index++) {
this.Nodes[index].marked = false
}
this.nodePathId = []
}
@Provide @Watch('refreshNodes')Nodes:Array<Node> = [new Node(0, "noMean", false, [X_CENTER,Y_CENTER])];
@Provide curNodesIndex:number = 0
@Provide Edges:Array<Edge> = []
@Provide curEdgesIndex:number = 0
@State resTime:number = 0
@State resDistance:number = 0
@Provide solutionData:solutionData = {
count:-1,
time:" ",
path:" ",
otherPath:[""]
}
//画板刷新
@Provide @Watch('refreshBoard')refresh:boolean = false
@State loop:string = "100%"
@Provide isStartToPath:boolean = false
@Provide nodePath:string = ""
@Provide @Watch('refreshNodePath')nodePathId:Array<number> = []
refreshNodePath(){
this.nodePath = ''
for (let index = 0; index < this.nodePathId.length; index++) {
this.nodePath += (this.Nodes[this.nodePathId[index]].label + `(${this.nodePathId[index]})`);
if (index != this.nodePathId.length - 1) {
this.nodePath += "->";
}
}
if(this.nodePathId.length == 0){
this.curNodesIndex = 0
}else{
this.curNodesIndex = this.nodePathId[this.nodePathId.length-1]
}
this.refresh = !this.refresh
Logger.i(JSON.stringify(this.nodePathId))
}
refreshNodes(){
this.curNodesIndex = 0
//重新给id排序
for (let index = 0; index < this.Nodes.length; index++) {
this.Nodes[index].id = index
}
this.Edges = cp.createEdgeData(this.Nodes)
//涉及到节点数组的变化先更新节点数组再更新连接数组再更新画板
this.refresh = !this.refresh
}
refreshBoard(){
this.loop = this.loop == "100%"?"101%":"100%"
}
@State isShow:boolean = true
build() {
Stack(){
Column(){
header({headerContent:this.headerContent,curHeaderContentIndex:$curHeaderContentIndex,resTime:$resTime,resDistance:$resDistance})
.width('100%')
.layoutWeight(1)
Divider()
.width('100%')
.strokeWidth(1)
.color(Color.Orange)
Row(){
imageShow({loop:this.loop,curEdgesIndex:$curEdgesIndex})
.width('80%')
.height('100%')
operateBar({headerContent:this.headerContent,curHeaderContentIndex:$curHeaderContentIndex})
.width('20%')
.height('100%')
}.width('100%').height('90%')
}.width('100%').height('100%').opacity(this.isShow?0.425:1)
if(this.isShow) {
Column() {
Text('正在初始化数据...')
.fontSize(40)
.onAppear(async () => {
await createCityInfo().then((info) => {
cityInfo = info;
})
this.Nodes = cp.createNodeData(3);
this.Edges = cp.createEdgeData(this.Nodes);
this.isShow = false;
})
}.width('100%').height('100%').justifyContent(FlexAlign.Center)
}
}
}
}

View File

@ -0,0 +1,22 @@
import hilog from '@ohos.hilog';
const DOMAIN = 0
const APP_TAG = 'TSPTestApp'
export class Logger {
public static d(message: string) {
hilog.debug(DOMAIN, APP_TAG, message)
}
public static i(message: string) {
hilog.info(DOMAIN, APP_TAG, message)
}
public static e(message: string) {
hilog.error(DOMAIN, APP_TAG, message)
}
public static w(message: string) {
hilog.warn(DOMAIN, APP_TAG, message)
}
}

View File

@ -0,0 +1,48 @@
import { cp } from "./customPath";
import Node from '../../common/dataStruct/class/Node';
import Edge from '../../common/dataStruct/class/Edge';
export class TSP {
path: Edge[]; // 从连接构造的路径
nodes: Node[]; // 节点对象列表
edges: Edge[]; // 连接列表
constructor() {
this.path = [];
this.nodes = [];
this.edges = [];
}
/**
* 创建节点数组和全连接数组
* @param nodeNum 节点数量
*/
createNodesAndEdges(nodeNum: number) {
this.path = [];
this.nodes = cp.createNodeData(nodeNum);
this.edges = cp.createEdgeData(this.nodes);
}
/**
* 创建随机 tsp 问题
*/
createRandomTspProblem() {
// 随机4到7个节点
const nodeNum: number = Math.floor(Math.random() * (7 - 4 + 1)) + 4;
this.createNodesAndEdges(nodeNum);
}
/**
* 创建随机路径
* @param nodeNum 节点数量
*/
createRandomPath(nodeNum: number) {
this.createNodesAndEdges(nodeNum);
let connOrder: number[] = Array.from({length: nodeNum}, (_: number, i: number) => i);
connOrder.sort(() => Math.random() - 0.5);
this.path = cp.setDisplayPath(connOrder)[0];
}
}
export let tsp: TSP = new TSP();

View File

@ -0,0 +1,574 @@
import { showToast } from "../functions/showToast"
import Node from '../../common/dataStruct/class/Node'
import Edge from '../../common/dataStruct/class/Edge'
import { BOTTOM_RIGHT_X, BOTTOM_RIGHT_Y, DRAW_NODE_SIZE, TOP_LEFT_X, TOP_LEFT_Y, CITY } from '../../common/CommonConstant';
import { cityInfo } from '../../pages/Index';
import { Graph, nodeInfo } from '../../common/dataStruct/interface/Graph';
import { deepCopyGraph } from '../functions/deepCopyGraph';
import { degreesToRadians } from '../functions/degreesToRadians';
let cityName: string[] = CITY.slice();
let usedCity: string[] = [];
let surplusCity: string[] = [];
export class CustomPath {
path: Edge[]; // 从连接构造的路径
pathDistanceCount: number; // 路径距离成本
pathTimeCount: number; // 路径时间成本
elapsedTime: number; // 运行时间
graph: Graph; // 邻接图
nodes: Node[]; // 节点对象列表
edges: Edge[]; // 连接列表
circles: number[][]; // 形成环路径
numberOfPasses: number; // 遍历次数
constructor(path: Edge[], pathDistanceCount: number, pathTimeCount: number, elapsedTime: number, graph: Graph, nodes: Node[], edges: Edge[]) {
this.path = path;
this.pathDistanceCount = pathDistanceCount;
this.pathTimeCount = pathTimeCount;
this.elapsedTime = elapsedTime;
this.graph = graph;
this.nodes = nodes;
this.edges = edges;
this.circles = [];
this.numberOfPasses = 0;
}
createNodeData(data: number): Node[] {
let num: number = data;
let ang = 360 / num;
let R = 140;
this.nodes = [];
cityName.sort(() => Math.random() - 0.5);
for (let i = 0; i < num; i++) {
let origin: [number, number] = [(BOTTOM_RIGHT_X - TOP_LEFT_X - DRAW_NODE_SIZE) / 2, (BOTTOM_RIGHT_Y - TOP_LEFT_Y - DRAW_NODE_SIZE) / 2];
let rad = degreesToRadians(i * ang);
const x = origin[0] + Math.cos(rad) * R | 0;
const y = origin[1] + Math.sin(rad) * R | 0;
this.nodes.push(new Node(i, cityName[i], false, [x, y], true));
}
usedCity = cityName.slice(0, num);
surplusCity = cityName.slice(num);
return this.nodes;
}
addNodeData(): Node[] {
let num: number = this.nodes.length + 1;
let ang = 360 / num;
let R = 140;
let origin: [number, number] = [(BOTTOM_RIGHT_X - TOP_LEFT_X - DRAW_NODE_SIZE) / 2, (BOTTOM_RIGHT_Y - TOP_LEFT_Y - DRAW_NODE_SIZE) / 2];
surplusCity.sort(() => Math.random() - 0.5);
this.nodes = this.nodes.map(node => {
let rad = degreesToRadians(node.id * ang);
const x = origin[0] + Math.cos(rad) * R | 0;
const y = origin[1] + Math.sin(rad) * R | 0;
node.coordinates = [x, y];
return node;
});
let rad = degreesToRadians((num - 1) * ang);
const x = origin[0] + Math.cos(rad) * R | 0;
const y = origin[1] + Math.sin(rad) * R | 0;
this.nodes.push(new Node(this.nodes.length, surplusCity[0] , false, [x, y], true));
usedCity = usedCity.concat(surplusCity[0]);
surplusCity = surplusCity.slice(1);
this.createEdgeData(this.nodes);
return this.nodes;
}
deleteNodeData(currentNodeId: number): Node[] {
// surplusCity = usedCity.slice(-1).concat(surplusCity);
// usedCity = usedCity.slice(0, -1);
// this.nodes.pop();
usedCity = usedCity.filter((city) => city != this.nodes[currentNodeId].label);
surplusCity = [this.nodes[currentNodeId].label].concat(surplusCity);
this.nodes = this.nodes.filter(node => node.id != currentNodeId);
this.nodes = this.nodes.map((node, index) => {
node.id = index;
return node;
});
this.createEdgeData(this.nodes);
return this.nodes;
}
regenerate() {
this.nodes = this.nodes.map(node => {
let x = Math.random()*(BOTTOM_RIGHT_X - TOP_LEFT_X - DRAW_NODE_SIZE);
let y = Math.random()*(BOTTOM_RIGHT_Y - TOP_LEFT_Y - DRAW_NODE_SIZE);
node.coordinates = [x, y];
return node;
})
}
createEdgeData(nodes: Node[]): Edge[] {
this.edges = [];
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const connectionId = this.edges.length + 1;
const node1Id = nodes[i].id;
const node2Id = nodes[j].id;
const visible = true;
const marked = false;
const label = `Connection-${connectionId}`;
// const distance = Math.floor(Math.random() * (1000 - 10 + 1)) + 10; // 生成10到1000的随机整数 --你需要设置实际的距离值
// const time = Math.floor(Math.random() * (720 - 60 + 1)) + 60; // 生成60到720的随机整数 --你需要设置实际的时间值
const distance = cityInfo[nodes[i].label][nodes[j].label].distance;
const time = cityInfo[nodes[i].label][nodes[j].label].time;
// console.log(`${nodes[i].label}->${nodes[j].label}: 距离为${distance},时间为${time}`);
this.edges.push(new Edge(connectionId, node1Id, node2Id, visible, marked, label, distance, time));
}
}
return this.edges;
}
connectTwoNodes(node1Id: number, node2Id: number) {
let nodeId: [number, number] = [node1Id, node2Id];
for (const edge of this.edges) {
if (nodeId.includes(edge.node1) && nodeId.includes(edge.node2)) {
if (!edge.visible) edge.visible = true;
else showToast("当前两节点已连接");
}
}
}
setDisplayPath(nodeIds: number[]): [Edge[], number, number] {
this.path = []; // 置空操作
this.pathDistanceCount = 0;
this.pathTimeCount = 0;
if (nodeIds.length) {
for (let i = 0; i < nodeIds.length; i++) {
let id1: number = 0;
let id2: number = 0;
if (i < nodeIds.length - 1) {
id1 = nodeIds[i];
id2 = nodeIds[i + 1];
} else {
if (nodeIds[0] == nodeIds[-1]) {
break;
} else {
id1 = nodeIds[i];
id2 = nodeIds[0];
}
}
let nodeId: [number, number] = [id1, id2]; // 用于寻找两节点的连接对象
for (const edge of this.edges) {
if (nodeId.includes(edge.node1) && nodeId.includes(edge.node2)) {
edge.marked = true;
this.path.push(edge);
this.pathDistanceCount += edge.distance;
this.pathTimeCount += edge.time;
}
}
}
}
return [this.path, this.pathDistanceCount, this.pathTimeCount];
}
clearPath() {
this.path = [];
this.pathDistanceCount = 0;
this.pathTimeCount = 0;
this.edges.forEach(edge => {
edge.marked = false; // 全部连接清除标记
})
}
undoPath() {
if (this.path.length) {
this.path[-1].marked = false; // 路径末尾连接清除标记
this.path.pop();
}
}
checkPathLegitimate(paths: number[]): boolean {
for (let i = 0; i < paths.length; i++) {
let node1Id: number = 0;
let node2Id: number = 0;
if (i < paths.length - 1) {
node1Id = paths[i];
node2Id = paths[i + 1];
} else {
node1Id = paths[i];
node2Id = paths[0];
}
if (node1Id == node2Id) {
return false;
}
let node1: Node = this.nodes[node1Id];
let node2: Node = this.nodes[node2Id];
if (!this.graphCheckConnectivity(node1, node2)[0]) {
showToast(`在节点${node1.label}与节点${node2.label}之间没有连接, 路径不合法, 请重新选择路径`);
return false;
}
}
let nonessentialNode: Node[] = this.nodes.filter(node => node.mustVisit == false);
let nonessentialNum: number = nonessentialNode.length;
let tempPaths: number[] = paths.filter(nodeId => { // 路径中所有必经节点构成数组
for (let i = 0; i < nonessentialNum; i++) {
if (nodeId == nonessentialNode[i].id) {
return false;
}
}
return true;
});
if (tempPaths.length != this.nodes.length - nonessentialNum) {
showToast(`所选路径未经所有必经节点城市, 路径不合法, 请重新选择路径`);
return false;
}
showToast("所选路径合法");
return true;
}
createGraph(nodes: Node[], isDistance: boolean = true): Graph {
this.graph = {};
nodes.forEach(node => {
if (node.mustVisit == true) {
this.graph[node.id] = [];
}
})
nodes.forEach(node => {
if (node.mustVisit == true) {
this.edges.forEach(edge => {
if (edge.visible) {
let nodeIds: [number, number] = [edge.node1, edge.node2];
if (nodeIds.includes(node.id) && nodes[nodeIds[0]].mustVisit == true && nodes[nodeIds[1]].mustVisit == true) {
let nodeId: number = nodeIds.find(id => id !== node.id) as number;
let weight: number = isDistance ? edge.distance : edge.time;
this.graph[node.id].push({ to: nodeId, weight: weight });
}
}
})
}
})
return this.graph;
}
dfs(nodeId: number, visited: Set<number>, path: number[], startNeighbors: Set<number>) {
try {
visited.add(nodeId);
path.push(nodeId);
if (path.length == Object.keys(this.graph).length && startNeighbors.has(path[path.length - 1])) {
this.circles.push(path.slice());
return;
}
for (const adjacentNodeInfo of this.graph[nodeId]) {
const neighbor: number = adjacentNodeInfo.to;
if (!visited.has(neighbor)) {
this.dfs(neighbor, visited, path, startNeighbors);
visited.delete(neighbor);
path.pop();
}
}
} catch {
console.log(this.circles.length.toString());
this.circles.length = 0;
return;
}
}
findCompleteCycles(graph: Graph, startNodeId: number = 0): number[][] {
this.circles = [];
let visited: Set<number> = new Set();
let startNeighborsArr: number[] = graph[startNodeId].map(nodeInfo => nodeInfo.to);
let startNeighbors: Set<number> = new Set(startNeighborsArr);
this.dfs(startNodeId, visited, [], startNeighbors);
return this.circles;
}
calculatePathCost(graph: Graph, path: number[]) {
let costValue = 0;
for (let i = 0; i < path.length - 1; i++) {
let currentNodeId: number = path[i];
let nextNodeId: number = path[i + 1];
if (!(graph[currentNodeId] && graph[currentNodeId].length === 0)) {
for (const adjacentNodeInfo of graph[currentNodeId]) {
if (adjacentNodeInfo.to == nextNodeId) {
costValue += adjacentNodeInfo.weight;
break;
}
}
} else break;
}
return costValue;
}
traversalSearchToPath(cp: CustomPath, graph: Graph, startNodeId: number = 0, _mustVisitNode?: number[]): [number[][], number, number, number] {
let startTime: number = Date.now();
let allPaths: number[][] = cp.findCompleteCycles(graph, startNodeId);
let numOfPasses = allPaths.length;
if (allPaths.length == 0) {
try {
throw new Error("当前图无法获取完整路径,请检查节点连接是否完整");
} catch {
showToast("当前图无法获取完整路径,请检查节点连接是否完整");
return [[], 0, 0, 0];
}
}
let minPathList: number[][] = [];
let minValue: number = Number.POSITIVE_INFINITY;
for (const permutation of allPaths) {
let currentPath = permutation.concat([startNodeId]);
let currentCostValue = cp.calculatePathCost(graph, currentPath);
if (currentCostValue < minValue) {
minValue = currentCostValue;
minPathList = [];
minPathList.push(currentPath.slice());
} else if (currentCostValue == minValue) {
minPathList.push(currentPath.slice());
}
}
let endTime: number = Date.now();
cp.elapsedTime = (endTime - startTime) * 1e-3; // 转换为秒
return [minPathList, minValue, cp.elapsedTime, numOfPasses];
}
greedySearchToPath(cp: CustomPath, graph: Graph, startNodeId: number = 0, _mustVisitNode?: number[]): [number[][], number, number, number] {
let startTime: number = Date.now();
let startNeighborsArr: number[] = graph[startNodeId].map(nodeInfo => nodeInfo.to);
let startNeighbors: Set<number> = new Set(startNeighborsArr);
let remainingNodes: Set<number> = new Set(Object.keys(graph).map(i => Number(i)));
remainingNodes.delete(startNodeId);
let currentNodeId = startNodeId;
let path: number[] = [startNodeId];
while (remainingNodes.size > 0) {
if (graph[currentNodeId].length === 0) break;
let nextNodeId: number = Number.NaN;
let minValue: number = Number.POSITIVE_INFINITY;
for (const adjacentNodeInfo of graph[currentNodeId]) {
if (adjacentNodeInfo.weight < minValue && !path.includes(adjacentNodeInfo.to)) {
minValue = adjacentNodeInfo.weight;
nextNodeId = adjacentNodeInfo.to;
}
}
if (Number.isNaN(nextNodeId)) {
try {
throw new Error(`当前id为${currentNodeId}的节点无直接路径到id为${Array.from(remainingNodes)[0]}的节点,贪心算法求解失败`);
} catch {
showToast(`当前id为${currentNodeId}的节点无直接路径到id为${Array.from(remainingNodes)[0]}的节点,贪心算法求解失败`);
return [[], 0, 0, 1];
}
}
path.push(nextNodeId);
remainingNodes.delete(nextNodeId);
currentNodeId = nextNodeId;
}
if (startNeighbors.has(path[path.length - 1])) {
path.push(startNodeId);
} else {
try {
throw new Error(`当前id为${path[path.length - 1]}的节点无直接路径到id为${startNodeId}的节点,贪心算法求解失败`);
} catch {
showToast(`当前id为${path[path.length - 1]}的节点无直接路径到id为${startNodeId}的节点,贪心算法求解失败`);
return [[], 0, 0, 1];
}
}
let endTime: number = Date.now();
cp.elapsedTime = (endTime - startTime) * 1e-3; // 转换为秒
return [[path], cp.calculatePathCost(graph, path), cp.elapsedTime, 1];
}
solveTsp(startNodeId: number = 0, func: (cp: CustomPath, graph: Graph, startNodeId: number) => [number[][], number, number, number]): [number[][], number, number, number] {
let startTime: number = Date.now();
let shortestDistance: number[] = [];
let pathList: number[][][] = [];
let remainGraph: Graph[] = [];
let nonessentialNode: Node[] = this.nodes.filter(node => node.mustVisit == false);
let nonessentialNum: number = nonessentialNode.length;
this.numberOfPasses = 0;
for (let i = 0; i < nonessentialNum; i++) {
if (startNodeId == nonessentialNode[i].id) {
try {
throw new Error("起始节点不能为非必经节点");
} catch {
showToast("起始节点不能为非必经节点");
return [[], 0, 0, 0];
}
}
}
let graph: Graph = this.createGraph(this.nodes);
let res: [number[][], number, number, number] = func(this, graph, startNodeId);
if (!res[0].length) return [[], 0, 0, 0];
pathList.push(res[0].slice());
shortestDistance.push(res[1]);
this.numberOfPasses += res[3];
remainGraph.push(deepCopyGraph(graph));
for (let i = 0; i < nonessentialNum; i++) {
this.nodes[nonessentialNode[i].id].mustVisit = true;
for (let j = i + 1; j < nonessentialNum; j++) {
this.nodes[nonessentialNode[j].id].mustVisit = true;
graph = this.createGraph(this.nodes);
res = func(this, graph, startNodeId);
if (!res[0].length) return [[], 0, 0, 0];
pathList.push(res[0].slice());
shortestDistance.push(res[1]);
this.numberOfPasses += res[3];
remainGraph.push(deepCopyGraph(graph));
this.nodes[nonessentialNode[j].id].mustVisit = false;
}
graph = this.createGraph(this.nodes);
res = func(this, graph, startNodeId);
if (!res[0].length) return [[], 0, 0, 0];
pathList.push(res[0].slice());
shortestDistance.push(res[1]);
this.numberOfPasses += res[3];
remainGraph.push(deepCopyGraph(graph));
this.nodes[nonessentialNode[i].id].mustVisit = false;
}
let index: number = shortestDistance.reduce((minIndex, currentValue, currentIndex, array) => {
return currentValue < array[minIndex] ? currentIndex : minIndex;
}, 0);
this.graph = deepCopyGraph(remainGraph[index]);
let endTime: number = Date.now();
this.elapsedTime = (endTime - startTime) * 1e-3; // 转换为秒
return [pathList[index], shortestDistance[index], this.elapsedTime, this.numberOfPasses];
}
graphCheckConnectivity(node1: Node, node2: Node): [boolean, number, number] {
if (this.nodes && this.edges && this.nodes.length != 0 && this.edges.length != 0) {
let find: number[] = [];
for (const node of this.nodes) {
find.push(node.id);
}
for (const edge of this.edges) {
if (edge.visible) {
let x: number = edge.node1;
let y: number = edge.node2;
while (x != find[x]) x = find[x];
while (y != find[y]) y = find[y];
if (x < y) find[y] = x;
else find[x] = y;
}
}
let x: number = node1.id;
let y: number = node2.id;
while (x != find[x]) x = find[x];
while (y != find[y]) y = find[y];
if (find[x] == find[y]) return [true, x, y];
else return [false, x, y];
} else {
try {
throw new Error("当前图无节点或无边");
} catch {
showToast("当前图无节点或无边");
return [false, -1, -1];
}
}
}
specificPathCost(path: Edge[], isDistance: boolean = true): number {
let minValue: number = 0;
if (!isDistance) {
for (const pathEle of path) {
minValue += pathEle.time;
}
return minValue;
} else {
for (const pathEle of path) {
minValue += pathEle.distance;
}
return minValue;
}
}
checkUserBest(path: Edge[], startNodeId: number = 0, isDistance: boolean = true): boolean {
let revalue: [number[][], number, number, number] = this.solveTsp(startNodeId, this.traversalSearchToPath);
let minPath: number[] = revalue[0][0];
if (path.length + 1 != minPath.length) return false;
let minValue: number = revalue[1];
let userMinValue = this.specificPathCost(path, isDistance);
if (minValue == userMinValue) return true;
else return false;
}
NodeIdsPathToEdgesPath(path: number[]): Edge[] {
let paths: Edge[] = [];
let len: number = path.length;
for (let i = 0; i < len - 1; i++) {
for (const edge of this.edges) {
if ((edge.node1 == path[i] && edge.node2 == path[i + 1]) || (edge.node1 == path[i + 1] && edge.node2 == path[i])) {
paths.push(edge);
break;
}
}
}
return paths;
}
EdgesPathToNodeIdsPath(path: Edge[], startNodeId: number = 0): number[] {
let paths: number[] = [startNodeId];
let nextNodeId: number = paths[0];
for (const pathEle of path) {
let nodeIds: number[] = [pathEle.node1, pathEle.node2];
if (nodeIds.includes(nextNodeId)) {
nextNodeId = nodeIds.find(id => id != nextNodeId) as number;
paths.push(nextNodeId);
}
}
return paths;
}
checkIsGreedy(path: Edge[], startNodeId: number = 0) {
let revalue: [number[][], number, number, number] = this.solveTsp(startNodeId, this.greedySearchToPath);
let greedyPath: number[] = revalue[0][0];
if (path.length + 1 != greedyPath.length) return false;
let userPath: number[] = this.EdgesPathToNodeIdsPath(path, startNodeId);
let len: number = userPath.length;
for (let i = 0; i < len; i++) {
if (userPath[i] != greedyPath[i]) {
try {
throw new Error(`正确的贪心节点顺序为${greedyPath.slice(0, -1)}\n你的第${i + 1}个节点${this.nodes[userPath[i]].label}不符合贪心规则。`);
} catch {
showToast(`正确的贪心节点顺序为${greedyPath.slice(0, -1)}\n你的第${i + 1}个节点${this.nodes[userPath[i]].label}不符合贪心规则。`);
return false;
}
};
}
return true;
}
shortestPathAlgorithm(startNodeId: number, endNodeId: number, isDistance: boolean = true): [number[], number] {
this.graph = this.createGraph(this.nodes, isDistance);
let distances: Record<number, number> = {}; // 记录从起始节点到每个节点的最短距离
let previous: Record<number, number> = {}; // 记录最短路径中每个节点的前驱节点
let visited: Set<number> = new Set(); // 存储已经处理过的节点
let priorityQueue: Array<nodeInfo> = []; // 优先队列,用于处理待访问的节点,按照距离排序
for (const nodeId of Object.keys(this.graph)) {
distances[nodeId] = Number.POSITIVE_INFINITY;
previous[nodeId] = -1;
}
distances[startNodeId] = 0;
priorityQueue.push({ to: startNodeId, weight: 0 });
while (priorityQueue.length > 0) {
priorityQueue.sort((a, b) => a.weight - b.weight); // 维护优先队列,保证每次队列中距离最小的节点在最前面
let currentNodeId = priorityQueue.shift()!.to;
if (currentNodeId === endNodeId) break;
if (visited.has(currentNodeId)) continue;
visited.add(currentNodeId);
for (const neighbor of this.graph[currentNodeId]) {
const neighborNodeId: number = neighbor.to;
const weight: number = neighbor.weight;
let newDist: number = distances[currentNodeId] + weight;
if (newDist < distances[neighborNodeId]) {
distances[neighborNodeId] = newDist;
previous[neighborNodeId] = currentNodeId;
priorityQueue.push({ to: neighborNodeId, weight: newDist });
}
}
}
let path: number[] = [];
let currentNodeId: number = endNodeId;
while (currentNodeId != -1) {
path.unshift(currentNodeId);
currentNodeId = previous[currentNodeId];
}
return [path, distances[endNodeId]];
}
}
export let cp: CustomPath = new CustomPath([], 0, 0, 0, {}, [], []);

View File

@ -0,0 +1,110 @@
import { CITY } from '../../common/CommonConstant';
import { CityInfo } from '../../common/dataStruct/interface/CityInfo';
import { Logger } from '../Logger';
import { getRealData } from './getRealData';
export async function createCityInfo(): Promise<CityInfo> {
//TODO 可注释掉第87、88行 再将第6行至85行注释打开 即可在指定的预览器参数下模拟程序
// let res: [string, string, number, number][] = [
// ["广州市", "深圳市", 123012, 10838],
// ["广州市", "珠海市", 118916, 9380],
// ["广州市", "汕头市", 408198, 20355],
// ["广州市", "佛山市", 24721, 4042],
// ["广州市", "韶关市", 214580, 10298],
// ["广州市", "湛江市", 412296, 17672],
// ["广州市", "肇庆市", 95526, 6740],
// ["广州市", "江门市", 79764, 6810],
// ["广州市", "茂名市", 330124, 16845],
// ["广州市", "惠州市", 132169, 10257],
// ["广州市", "梅州市", 375039, 23471],
// ["广州市", "汕尾市", 250695, 22951],
// ["深圳市", "珠海市", 96524, 6856],
// ["深圳市", "汕头市", 327090, 16705],
// ["深圳市", "佛山市", 131396, 7894],
// ["深圳市", "韶关市", 314989, 14934],
// ["深圳市", "湛江市", 458270, 17171],
// ["深圳市", "肇庆市", 207397, 12600],
// ["深圳市", "江门市", 117755, 7185],
// ["深圳市", "茂名市", 379777, 14772],
// ["深圳市", "惠州市", 85900, 8126],
// ["深圳市", "梅州市", 333237, 19392],
// ["深圳市", "汕尾市", 160776, 10851],
// ["珠海市", "汕头市", 414804, 20789],
// ["珠海市", "佛山市", 115634, 8714],
// ["珠海市", "韶关市", 330794, 18384],
// ["珠海市", "湛江市", 399887, 17975],
// ["珠海市", "肇庆市", 170341, 10310],
// ["珠海市", "江门市", 74683, 6582],
// ["珠海市", "茂名市", 321394, 15731],
// ["珠海市", "惠州市", 166006, 16927],
// ["珠海市", "梅州市", 418175, 22730],
// ["珠海市", "汕尾市", 248490, 14697],
// ["汕头市", "佛山市", 428134, 21849],
// ["汕头市", "韶关市", 439125, 28337],
// ["汕头市", "湛江市", 777628, 31482],
// ["汕头市", "肇庆市", 503503, 19173],
// ["汕头市", "江门市", 437113, 21891],
// ["汕头市", "茂名市", 699135, 28654],
// ["汕头市", "惠州市", 281852, 13534],
// ["汕头市", "梅州市", 150580, 10931],
// ["汕头市", "汕尾市", 178082, 9358],
// ["佛山市", "韶关市", 236498, 14079],
// ["佛山市", "湛江市", 387931, 16579],
// ["佛山市", "肇庆市", 83082, 6325],
// ["佛山市", "江门市", 57582, 5669],
// ["佛山市", "茂名市", 306652, 14224],
// ["佛山市", "惠州市", 152376, 11891],
// ["佛山市", "梅州市", 395246, 25040],
// ["佛山市", "汕尾市", 270551, 21935],
// ["韶关市", "湛江市", 597469, 29973],
// ["韶关市", "肇庆市", 266051, 13269],
// ["韶关市", "江门市", 297901, 15143],
// ["韶关市", "茂名市", 512263, 25970],
// ["韶关市", "惠州市", 241728, 12976],
// ["韶关市", "梅州市", 351402, 17854],
// ["韶关市", "汕尾市", 353072, 17957],
// ["湛江市", "肇庆市", 337242, 17455],
// ["湛江市", "江门市", 352004, 13773],
// ["湛江市", "茂名市", 88410, 5807],
// ["湛江市", "惠州市", 525222, 21359],
// ["湛江市", "梅州市", 772108, 35020],
// ["湛江市", "汕尾市", 611969, 26177],
// ["肇庆市", "江门市", 96870, 6183],
// ["肇庆市", "茂名市", 250335, 13916],
// ["肇庆市", "惠州市", 227448, 15215],
// ["肇庆市", "梅州市", 470318, 28141],
// ["肇庆市", "汕尾市", 347288, 23119],
// ["江门市", "茂名市", 273884, 11126],
// ["江门市", "惠州市", 175884, 14701],
// ["江门市", "梅州市", 425305, 22731],
// ["江门市", "汕尾市", 271930, 18071],
// ["茂名市", "惠州市", 446878, 18932],
// ["茂名市", "梅州市", 693764, 33390],
// ["茂名市", "汕尾市", 533625, 25063],
// ["惠州市", "梅州市", 253720, 11521],
// ["惠州市", "汕尾市", 121252, 8521],
// ["梅州市", "汕尾市", 216441, 11933]
// ]
let res: [string, string, number, number][] = await getRealData();
Logger.i(JSON.stringify(res));
let cityName: string[] = CITY.slice();
let cityInfo: CityInfo = {};
cityName.forEach((city) => {
cityInfo[city] = {};
})
res.forEach((urbanTransitionInfo) => {
cityInfo[urbanTransitionInfo[0]][urbanTransitionInfo[1]] = { distance: urbanTransitionInfo[2], time: urbanTransitionInfo[3] };
if (!cityInfo[urbanTransitionInfo[1]][urbanTransitionInfo[0]]) cityInfo[urbanTransitionInfo[1]][urbanTransitionInfo[0]] = { distance: urbanTransitionInfo[2], time: urbanTransitionInfo[3] };
})
// console.log(Object.keys(cityInfo).toString())
// for (const i of Object.keys(cityInfo)) {
// console.log(Object.keys(cityInfo[i]).toString())
// for (const j of Object.keys(cityInfo[i])) {
// console.log(`${i}->${j}: 距离为${cityInfo[i][j].distance},时间为${cityInfo[i][j].time}`);
// }
// }
return cityInfo;
}

View File

@ -0,0 +1,17 @@
import { Graph, nodeInfo } from '../../common/dataStruct/interface/Graph';
export function deepCopyGraph(graph: Graph): Graph {
const newGraph: Graph = {};
for (const nodeId of Object.keys(graph)) {
// 深拷贝每个 nodeInfo 数组
newGraph[nodeId] = graph[nodeId].map((node: nodeInfo) => {
let nodeMessage: nodeInfo ={
to: node.to,
weight: node.weight
};
return nodeMessage;
});
}
return newGraph;
}

View File

@ -0,0 +1,3 @@
export function degreesToRadians(degrees: number): number {
return degrees * Math.PI / 180;
}

View File

@ -0,0 +1,33 @@
import { DRAW_NODE_SIZE, NORMAL_CONTEXT, SELECTED_CONTEXT } from '../../common/CommonConstant'
import Edge from '../../common/dataStruct/class/Edge'
import { cp } from '../algorithm/customPath'
import { Logger } from '../Logger'
export let drawEdge = (edge:Edge,isSelectedPath:boolean)=>{
let context = isSelectedPath?SELECTED_CONTEXT:NORMAL_CONTEXT
if(isSelectedPath){
if(edge.marked){
context.moveTo(cp.nodes[edge.node1].coordinates[0] + DRAW_NODE_SIZE/2,cp.nodes[edge.node1].coordinates[1] + DRAW_NODE_SIZE/2)
context.lineTo(cp.nodes[edge.node2].coordinates[0] + DRAW_NODE_SIZE/2,cp.nodes[edge.node2].coordinates[1] + DRAW_NODE_SIZE/2)
context.stroke()
}
}else{
if(edge.visible){
context.moveTo(cp.nodes[edge.node1].coordinates[0] + DRAW_NODE_SIZE/2,cp.nodes[edge.node1].coordinates[1] + DRAW_NODE_SIZE/2)
context.lineTo(cp.nodes[edge.node2].coordinates[0] + DRAW_NODE_SIZE/2,cp.nodes[edge.node2].coordinates[1] + DRAW_NODE_SIZE/2)
context.stroke()
}
}
context.stroke()
}
export let drawEdgeList = (edgeList:Array<Edge>,isSelectedPath:boolean)=>{
if(edgeList.length == 0){
return
}
for(let index = 0;index < edgeList.length;index++){
drawEdge(edgeList[index],isSelectedPath)
}
}

View File

@ -0,0 +1,40 @@
import Node from '../../common/dataStruct/class/Node'
import { BOTTOM_RIGHT_X, BOTTOM_RIGHT_Y, DRAW_NODE_SIZE, TOP_LEFT_X, TOP_LEFT_Y,
X_CENTER,
Y_CENTER
} from '../../common/CommonConstant'
export let drawNode = (node:Node,context:CanvasRenderingContext2D)=>{
if(!node.mustVisit){
context.fillRect(node.coordinates[0],node.coordinates[1],DRAW_NODE_SIZE,DRAW_NODE_SIZE)
}else{
context.strokeRect(node.coordinates[0],node.coordinates[1],DRAW_NODE_SIZE,DRAW_NODE_SIZE)
}
let bias = getTextBiasValue(node)
context.fillText(node.label,node.coordinates[0] + DRAW_NODE_SIZE/2 + bias[0],node.coordinates[1] + DRAW_NODE_SIZE/2 + bias[1])
context.stroke()
}
export let drawNodeList = (nodeList:Array<Node>,context:CanvasRenderingContext2D)=>{
if(nodeList.length == 0){
return
}
for (let index = 0; index < nodeList.length; index++) {
drawNode(nodeList[index],context)
}
}
let getTextBiasValue = (node:Node)=>{
let bias:[number,number] = [0,0]
if(node.coordinates[0] < X_CENTER){
bias[0] = -40
}
if(node.coordinates[1] > Y_CENTER){
bias[1] = 25
}
return bias
}

View File

@ -0,0 +1,147 @@
import { CITY, KEY } from '../../common/CommonConstant'
import http from '@ohos.net.http'
import { Logger } from '../Logger'
import fs from '@ohos.file.fs';
import picker from '@ohos.file.picker';
import { BusinessError } from '@ohos.base';
import process from '@ohos.process';
import { showToast } from './showToast';
import { Data } from '../../common/dataStruct/class/Data';
export let getRealData = async ()=>{
let res:Array<[string,string,number,number]> = []
let loop:boolean = AppStorage.get("isOpFile") as boolean
if(loop){
res = await getRealDataFromFile()
}else{
let str = ""
for(let i = 0;i < CITY.length;i++){
for(let j = i + 1;j < CITY.length;j++){
let tmp = await getRealDataFromMap(CITY[i],CITY[j])
res.push(tmp)
str += `${tmp[0]} ${tmp[1]} ${tmp[2]} ${tmp[3]}`
str += '\n'
}
}
await writeDocument(str)
}
return res;
}
/**
* 从地图获取真实数据
*/
let getRealDataFromMap = async (cityName1:string,cityName2:string)=>{
let res:[string,string,number,number] = ["","",0,0]
//获取经纬度
let getLocation1_url = `https://restapi.amap.com/v3/geocode/geo?address=${cityName1}&key=${KEY}`
let getLocation2_url = `https://restapi.amap.com/v3/geocode/geo?address=${cityName2}&key=${KEY}`
let loc1 = await getLoc(getLocation1_url)
let loc2 = await getLoc(getLocation2_url)
//获取最短路径
let getPath_url = `https://restapi.amap.com/v3/direction/driving?origin=${loc1}&destination=${loc2}&strategy=2&key=${KEY}`
let path = await getPath(getPath_url)
res[0] = cityName1
res[1] = cityName2
res[2] = path[0]
res[3] = path[1]
Logger.i(JSON.stringify(res))
return res
}
/**
* 从文件获取真实数据
*/
let getRealDataFromFile = async ()=>{
let dataList:Array<[string,string,number,number]> = []
//读取文件
let res = await readDocument(dataList)
if(!res){
setTimeout(()=>{
showToast("数据初始化异常,将自动关闭程序")
setTimeout(()=>{
let pro = new process.ProcessManager();
pro.exit(0);
},2000)
},2000)
}
return dataList
}
let getLoc = async (url:string)=>{
try {
let h = http.createHttp()
let res = await h.request(url)
//JSON
let json = res.result as string
let i = json.indexOf("location")
let loc = json.slice(i + 11,i+31)
return loc;
} catch (e) {
Logger.e(JSON.stringify(e))
return ""
}
}
let getPath = async (url:string)=>{
let h = http.createHttp()
let res = await h.request(url)
//JSON
let json = res.result as string
let obj:Data = JSON.parse(json)
let loc:[number,number] = [Number(obj.route.paths[0].distance),Number(obj.route.paths[0].duration)]
return loc;
}
let writeDocument = async (str:string)=>{
let documentPicker = new picker.DocumentViewPicker();
let uris:Array<string> = []
let DocumentSaveOptions = new picker.DocumentSaveOptions();
DocumentSaveOptions.newFileNames = ['cityData.txt']
uris = await documentPicker.save(DocumentSaveOptions)
let uri = uris[0]
let file = fs.openSync(uri, fs.OpenMode.WRITE_ONLY);
fs.writeSync(file.fd,"THIS IS A TAG\n")
let writeStr = str
fs.writeSync(file.fd, writeStr);
fs.closeSync(file);
}
let readDocument = async (dataList:Array<[string,string,number,number]>)=>{
let documentPicker = new picker.DocumentViewPicker()
let DocumentSelectOptions = new picker.DocumentSelectOptions();
let DocumentSelectResult: Array<string> = await documentPicker.select(DocumentSelectOptions)
if(DocumentSelectResult.length == 0){
showToast("您未选择任何文件!将无法获取到数据")
return false
}
let uri = DocumentSelectResult[0]
let file = fs.openSync(uri,fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)
//已经获取到选择的文件 进行读取操作
let arrayBuffer = new ArrayBuffer(8)
let size = 0
let dSize = 0
let str = fs.readTextSync(file.path)
//解析路径
fs.closeSync(file)
//解析文件样本点
let arr = str.split('\n')
if(arr[0] != "THIS IS A TAG"){
showToast("读取文件失败,请确认文件是否选择正确或是否损坏")
return false
}
for (let index = 1; index < arr.length - 1; index++) {
let res = arr[index].split(' ')
let item:[string,string,number,number] = [res[0],res[1],Number(res[2]),Number(res[3])]
dataList.push(item)
}
return true
}

View File

@ -0,0 +1,8 @@
import promptAction from '@ohos.promptAction'
export let showToast = (message:string,duration:number = 2000)=>{
promptAction.showToast({
message:message,
duration:duration
})
}

View File

@ -0,0 +1,49 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET"
},
{
"name": "ohos.permission.FILE_ACCESS_MANAGER"
},
{
"name": "ohos.permission.GET_BUNDLE_INFO_PRIVILEGED"
}
],
"deviceTypes": [
"default",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"orientation": "landscape",
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,5 @@
{
"src": [
"pages/Index"
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
}
]
}

View File

@ -0,0 +1,35 @@
import hilog from '@ohos.hilog';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}

View File

@ -0,0 +1,5 @@
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}

View File

@ -0,0 +1,50 @@
import UIAbility from '@ohos.app.ability.UIAbility';
import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry';
import hilog from '@ohos.hilog';
import { Hypium } from '@ohos/hypium';
import testsuite from '../test/List.test';
import window from '@ohos.window';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
export default class TestAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate');
hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? '');
hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? '');
let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator;
abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator();
let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs;
abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments();
hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!');
Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite);
}
onDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage) {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate');
windowStage.loadContent('testability/pages/Index', (err, data) => {
if (err.code) {
hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
return;
}
hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s',
JSON.stringify(data) ?? '');
});
}
onWindowStageDestroy() {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy');
}
onForeground() {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground');
}
onBackground() {
hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground');
}
}

View File

@ -0,0 +1,17 @@
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}

View File

@ -0,0 +1,47 @@
import hilog from '@ohos.hilog';
import TestRunner from '@ohos.application.testRunner';
import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry';
import Want from '@ohos.app.ability.Want';
let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator | undefined = undefined
let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs | undefined = undefined
async function onAbilityCreateCallback() {
hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback');
}
async function addAbilityMonitorCallback(err : Error) {
hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? '');
}
export default class OpenHarmonyTestRunner implements TestRunner {
constructor() {
}
onPrepare() {
hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare ');
}
async onRun() {
hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run');
abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments()
abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator()
const bundleName = abilityDelegatorArguments.bundleName;
const testAbilityName = 'TestAbility';
let lMonitor: AbilityDelegatorRegistry.AbilityMonitor = {
abilityName: testAbilityName,
onAbilityCreate: onAbilityCreateCallback,
};
abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback)
const want: Want = {
bundleName: bundleName,
abilityName: testAbilityName
};
abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator();
abilityDelegator.startAbility(want, (err, data) => {
hilog.info(0x0000, 'testTag', 'startAbility : err : %{public}s', JSON.stringify(err) ?? '');
hilog.info(0x0000, 'testTag', 'startAbility : data : %{public}s',JSON.stringify(data) ?? '');
})
hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end');
}
}

View File

@ -0,0 +1,37 @@
{
"module": {
"name": "entry_test",
"type": "feature",
"description": "$string:module_test_desc",
"mainElement": "TestAbility",
"deviceTypes": [
"default",
"tablet"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:test_pages",
"abilities": [
{
"name": "TestAbility",
"srcEntry": "./ets/testability/TestAbility.ets",
"description": "$string:TestAbility_desc",
"icon": "$media:icon",
"label": "$string:TestAbility_label",
"exported": true,
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"skills": [
{
"actions": [
"action.system.home"
],
"entities": [
"entity.system.home"
]
}
]
}
]
}
}

View File

@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}

View File

@ -0,0 +1,16 @@
{
"string": [
{
"name": "module_test_desc",
"value": "test ability description"
},
{
"name": "TestAbility_desc",
"value": "the test ability"
},
{
"name": "TestAbility_label",
"value": "test label"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -0,0 +1,5 @@
{
"src": [
"testability/pages/Index"
]
}

View File

@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}

View File

@ -0,0 +1,33 @@
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest',() => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}

View File

@ -0,0 +1,18 @@
{
"hvigorVersion": "3.0.9",
"dependencies": {
"@ohos/hvigor-ohos-plugin": "3.0.9"
},
"execution": {
// "daemon": true, /* Enable daemon compilation. Default: true */
// "incremental": true, /* Enable incremental compilation. Default: true */
// "parallel": true, /* Enable parallel compilation. Default: true */
// "typeCheck": false, /* Enable typeCheck. Default: false */
},
"logging": {
// "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */
},
"debugging": {
// "stacktrace": false /* Disable stacktrace compilation. Default: false */
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
import { appTasks } from '@ohos/hvigor-ohos-plugin';
export default {
system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
plugins:[] /* Custom plugin to extend the functionality of Hvigor. */
}

48
drumming/drummer/hvigorw Normal file
View File

@ -0,0 +1,48 @@
#!/bin/bash
# ----------------------------------------------------------------------------
# Hvigor startup script, version 1.0.0
#
# Required ENV vars:
# ------------------
# NODE_HOME - location of a Node home dir
# or
# Add /usr/local/nodejs/bin to the PATH environment variable
# ----------------------------------------------------------------------------
HVIGOR_APP_HOME="`pwd -P`"
HVIGOR_WRAPPER_SCRIPT=${HVIGOR_APP_HOME}/hvigor/hvigor-wrapper.js
warn() {
echo ""
echo -e "\033[1;33m`date '+[%Y-%m-%d %H:%M:%S]'`$@\033[0m"
}
error() {
echo ""
echo -e "\033[1;31m`date '+[%Y-%m-%d %H:%M:%S]'`$@\033[0m"
}
fail() {
error "$@"
exit 1
}
# Determine node to start hvigor wrapper script
if [ -n "${NODE_HOME}" ];then
EXECUTABLE_NODE="${NODE_HOME}/bin/node"
if [ ! -x "$EXECUTABLE_NODE" ];then
fail "ERROR: NODE_HOME is set to an invalid directory,check $NODE_HOME\n\nPlease set NODE_HOME in your environment to the location where your nodejs installed"
fi
else
EXECUTABLE_NODE="node"
which ${EXECUTABLE_NODE} > /dev/null 2>&1 || fail "ERROR: NODE_HOME is not set and not 'node' command found in your path"
fi
# Check hvigor wrapper script
if [ ! -r "$HVIGOR_WRAPPER_SCRIPT" ];then
fail "ERROR: Couldn't find hvigor/hvigor-wrapper.js in ${HVIGOR_APP_HOME}"
fi
# start hvigor-wrapper script
exec "${EXECUTABLE_NODE}" \
"${HVIGOR_WRAPPER_SCRIPT}" "$@"

View File

@ -0,0 +1,64 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Hvigor startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
set WRAPPER_MODULE_PATH=%APP_HOME%\hvigor\hvigor-wrapper.js
set NODE_EXE=node.exe
goto start
:start
@rem Find node.exe
if defined NODE_HOME goto findNodeFromNodeHome
%NODE_EXE% --version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: NODE_HOME is not set and no 'node' command could be found in your PATH.
echo.
echo Please set the NODE_HOME variable in your environment to match the
echo location of your NodeJs installation.
goto fail
:findNodeFromNodeHome
set NODE_HOME=%NODE_HOME:"=%
set NODE_EXE_PATH=%NODE_HOME%/%NODE_EXE%
if exist "%NODE_EXE_PATH%" goto execute
echo.
echo ERROR: NODE_HOME is not set and no 'node' command could be found in your PATH.
echo.
echo Please set the NODE_HOME variable in your environment to match the
echo location of your NodeJs installation.
goto fail
:execute
@rem Execute hvigor
"%NODE_EXE%" "%WRAPPER_MODULE_PATH%" %*
if "%ERRORLEVEL%" == "0" goto hvigorwEnd
:fail
exit /b 1
:hvigorwEnd
if "%OS%" == "Windows_NT" endlocal
:end

View File

@ -0,0 +1,13 @@
{
"lockfileVersion": 1,
"ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
"specifiers": {
"@ohos/hypium@1.0.11": "@ohos/hypium@1.0.11"
},
"packages": {
"@ohos/hypium@1.0.11": {
"resolved": "https://repo.harmonyos.com/ohpm/@ohos/hypium/-/hypium-1.0.11.tgz",
"integrity": "sha512-KawcLnv43C3QIYv1UbDnKCFX3MohtDxGuFvzlUxT/qf2DBilR56Ws6zrj90LdH6PjloJQwOPESuBQIHBACAK7w=="
}
}
}

View File

@ -0,0 +1,12 @@
{
"license": "",
"devDependencies": {
"@ohos/hypium": "1.0.11"
},
"author": "",
"name": "drawpart3",
"description": "Please describe the basic information.",
"main": "",
"version": "1.0.0",
"dependencies": {}
}

BIN
drumming/figs/evolution.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
drumming/figs/overall.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

BIN
drumming/figs/relation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB