my_shallow_water_project/
├── python-backend/
│ ├── requirements.txt
│ ├── shallow_water_sim.py
│ └── app.py
├── vue-frontend/
│ ├── package.json
│ ├── vite.config.js
│ ├── src/
│ │ ├── main.js
│ │ ├── App.vue
│ │ ├── views/
│ │ │ └── HomeView.vue
│ │ └── assets/
│ │ └── style.css
│ └── public/
python-backend/requirements.txt
Flask==2.3.2
numpy==1.24.3
matplotlib==3.7.1
shapely==2.0.1
python-backend/shallow_water_sim.py
import numpy as np
import os, json
import matplotlib.pyplot as plt
from shapely.geometry import Polygon, mapping
NX, NY = 100, 100
DX, DY = 30.0, 30.0
DT = 1.0
G = 9.81
DRY_TOL = 1e-3
CFL = 0.5
SIM_TIME = 100
DEM_FILE = "data/dem.npy"
OUTPUT_DIR = "output"
MANNING_N = 0.03
INITIAL_WATER_LEVEL = 15.0
def load_dem(fp):
dem = np.load(fp)
return dem
def initialize_water(dem):
return np.maximum(INITIAL_WATER_LEVEL - dem, 0.0)
def compute_time_step(u, v, h):
ws = np.sqrt(G * h) + np.sqrt(u**2 + v**2)
m = np.nanmax(ws)
if m < 1e-6:
return DT
return min(CFL * min(DX, DY) / m, DT)
def boundary_condition(u, v, h, dem):
u[0,:] = u[1,:]
u[-1,:] = u[-2,:]
u[:,0] = u[:,1]
u[:,-1] = u[:,-2]
v[0,:] = v[1,:]
v[-1,:] = v[-2,:]
v[:,0] = v[:,1]
v[:,-1] = v[:,-2]
h[0,:] = h[1,:]
h[-1,:] = h[-2,:]
h[:,0] = h[:,1]
h[:,-1] = h[:,-2]
def manning_friction(u, v, h, n=MANNING_N):
mask = (h > DRY_TOL)
sp2 = u**2 + v**2
fc = G * n**2 / np.power(h, 4.0/3.0, where=mask)
fc[~mask] = 0.0
return fc, sp2
def shallow_water_step(u, v, h, dem, dt):
fc, sp2 = manning_friction(u, v, h)
qx = u * h
qy = v * h
h2 = np.copy(h)
u2 = np.copy(u)
v2 = np.copy(v)
dqx_dx = (np.roll(qx, -1, axis=1) - qx) / DX
dqy_dy = (np.roll(qy, -1, axis=0) - qy) / DY
h2 -= dt * (dqx_dx + dqy_dy)
mask = (h2 > DRY_TOL)
sp = np.sqrt(sp2)
fx = fc * u * sp
fy = fc * v * sp
u2[mask] = u[mask] - dt * fx[mask]
v2[mask] = v[mask] - dt * fy[mask]
h2[h2 < DRY_TOL] = 0.0
u2[~mask] = 0.0
v2[~mask] = 0.0
return u2, v2, h2
def create_inundation_polygon(h, stepi):
fig, ax = plt.subplots()
X = np.arange(0, NX*DX, DX)
Y = np.arange(0, NY*DY, DY)
cc = ax.contourf(X, Y, h[::-1,:], [DRY_TOL, 1e9], cmap='Blues')
polys = []
for c in cc.collections:
for p in c.get_paths():
v = p.vertices
poly = Polygon(v)
polys.append(poly)
from shapely.ops import unary_union
if len(polys) == 0:
plt.close(fig)
return
pu = unary_union(polys) if len(polys) > 1 else polys[0]
geo = mapping(pu)
fn = os.path.join(OUTPUT_DIR, f"inundation_{stepi:04d}.geojson")
with open(fn, 'w') as f:
json.dump({
"type": "Feature",
"geometry": geo,
"properties": {"step": stepi}
}, f)
plt.close(fig)
def run_simulation():
dem = load_dem(DEM_FILE)
h = initialize_water(dem)
u = np.zeros_like(h)
v = np.zeros_like(h)
if not os.path.exists(OUTPUT_DIR):
os.makedirs(OUTPUT_DIR)
t = 0.0
cnt = 0
while t < SIM_TIME:
boundary_condition(u, v, h, dem)
dt_ = compute_time_step(u, v, h)
dt_ = min(dt_, SIM_TIME - t)
u, v, h = shallow_water_step(u, v, h, dem, dt_)
fn = f"step_{cnt:04d}.npz"
np.savez(os.path.join(OUTPUT_DIR, fn), u=u, v=v, h=h, time=t+dt_)
if cnt % 5 == 0:
create_inundation_polygon(h, cnt)
t += dt_
cnt += 1
def main():
run_simulation()
if __name__ == "__main__":
main()
python-backend/app.py
from flask import Flask, jsonify, request
import os, glob, json, threading
from shallow_water_sim import run_simulation
app = Flask(__name__)
@app.post("/api/start_sim")
def start_sim():
def do_sim():
run_simulation()
t = threading.Thread(target=do_sim)
t.start()
return jsonify({"message": "模拟已启动"})
@app.get("/api/inundation_list")
def inundation_list():
files = glob.glob("output/inundation_*.geojson")
file_names = [os.path.basename(f) for f in files]
return jsonify({"files": file_names})
@app.get("/api/inundation_geojson")
def inundation_geojson():
fname = request.args.get('file')
if not fname:
return jsonify({"error": "file param missing"}), 400
path = os.path.join("output", fname)
if not os.path.exists(path):
return jsonify({"error": "file not found"}), 404
with open(path, 'r') as f:
data = json.load(f)
return jsonify(data)
if __name__=="__main__":
app.run(debug=True)
vue-frontend/package.json
{
"name": "vue3-shallow-water",
"version": "1.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"vue": "^3.3.4",
"axios": "^1.4.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"vite": "^4.3.2"
}
}
vue-frontend/vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:5000',
changeOrigin: true
}
}
}
})
vue-frontend/src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import './assets/style.css'
const app = createApp(App)
app.mount('#app')
vue-frontend/src/App.vue
<template>
<div id="app">
<HomeView />
</div>
</template>
<script>
import HomeView from './views/HomeView.vue'
export default {
name: 'App',
components: { HomeView }
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
margin: 0 auto;
}
</style>
vue-frontend/src/views/HomeView.vue
<template>
<div class="home">
<h1>Vue 3 - Shallow Water Simulation</h1>
<button @click="startSimulation">启动模拟</button>
<p v-if="statusMessage">后端状态: {{ statusMessage }}</p>
<button @click="fetchInundationList">刷新淹没文件列表</button>
<h2>已生成的淹没文件:</h2>
<ul>
<li v-for="(file, idx) in inundationFiles" :key="idx">
<button @click="fetchGeoJSON(file)">{{ file }}</button>
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'HomeView',
data() {
return {
statusMessage: '',
inundationFiles: []
}
},
methods: {
async startSimulation() {
try {
const res = await axios.post('/api/start_sim')
this.statusMessage = res.data.message || '已请求启动模拟'
} catch (err) {
console.error(err)
this.statusMessage = '启动失败'
}
},
async fetchInundationList() {
try {
const res = await axios.get('/api/inundation_list')
this.inundationFiles = res.data.files || []
} catch (err) {
console.error(err)
}
},
async fetchGeoJSON(fname) {
try {
const res = await axios.get('/api/inundation_geojson', {
params: { file: fname }
})
console.log('GeoJSON内容:', res.data)
} catch (err) {
console.error(err)
}
}
},
mounted() {
this.fetchInundationList()
}
}
</script>
<style scoped>
.home {
padding: 1rem;
}
</style>
vue-frontend/src/assets/style.css
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}