stash some shit

Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
This commit is contained in:
Frank Villaro-Dixon 2024-05-13 22:27:52 +02:00
parent 3a3d997d49
commit 311ce09e15
23 changed files with 3454 additions and 0 deletions

3
ui/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
db.sqlite
node_modules/
public/rivers.json

8
ui/Dockerfile Normal file
View file

@ -0,0 +1,8 @@
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

5
ui/README.md Normal file
View file

@ -0,0 +1,5 @@
# vue3-openlayers Starter Template
See [vue3-openlayers](https://github.com/MelihAltintas/vue3-openlayers)
You can use this starter template as a playground or for providing reproduction steps, when opening new [issues](https://github.com/MelihAltintas/vue3-openlayers/issues).

24
ui/_gitignore Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

37
ui/generate-db.py Normal file
View file

@ -0,0 +1,37 @@
import json
import sqlite3
def db_to_js(db_name: str):
conn = sqlite3.connect(db_name)
c = conn.cursor()
c.execute("SELECT wayid, name, length, start_ele, end_ele, slope_correct, slope_confidence, centroid_lat, centroid_lon FROM rivers")
rows = c.fetchall()
ret = []
for river in rows:
sc = river[5]
if sc == 1:
#print(f"OK: {river}")
continue
r = {
'wayid': river[0],
'name': river[1],
'length': river[2],
'start_ele': river[3],
'end_ele': river[4],
'slope_correct': river[5],
'slope_confidence': river[6],
'centroid_lat': river[7],
'centroid_lon': river[8]
}
if r['slope_confidence'] > 0.4:
print(f"KO: {r}")
if r['start_ele'] + 20 < r['end_ele']:
print(f"KO: {r}")
ret.append(r)
return ret
if __name__ == '__main__':
rivers = db_to_js('db.sqlite')
with open('public/rivers.json', 'w') as f:
f.write(json.dumps(rivers, indent=1))

13
ui/index.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OSM QA: uphill rivers</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2650
ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

26
ui/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "vue3-openlayers-starter",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode development",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.5.1",
"bootstrap": "^5.3.2",
"chart.js": "^4.4.0",
"vue": "^3.2.47",
"vue-chartjs": "^5.2.0",
"vue-router": "^4.2.5",
"vue3-openlayers": "*"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^4.9.3",
"vite": "^4.2.1",
"vue-tsc": "^1.2.0"
}
}

1
ui/public/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
rivers.json

0
ui/public/.gitkeep Normal file
View file

23
ui/src/App.vue Normal file
View file

@ -0,0 +1,23 @@
<template>
<router-link :to="{ path: `/` }">
<h2 class="text-center">Openstreetmap QA: uphill rivers</h2>
</router-link>
<router-view />
</template>
<script>
export default {
name: "App",
components: {},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin-top: 60px;
}
</style>

7
ui/src/App2.vue Normal file
View file

@ -0,0 +1,7 @@
<script setup lang="ts">
import TheMap from './components/TheMap.vue';
</script>
<template>
<TheMap />
</template>

0
ui/src/assets/.gitkeep Normal file
View file

View file

@ -0,0 +1,246 @@
<template>
<notifications
class="mt-3 ms-3"
:duration="2000"
:width="250"
animation-name="v-fade-left"
position="top left"
/>
<div class="container my-5">
<h3 class="text-center">Liste des clusters</h3>
<!--
<div class="row d-flex justify-content-center">
<div class="col-xs-12 col-lg-6 my-3">
<div class="form-group mb-3">
<label for="todo" class="form-label">Add ToDo</label>
<div class="row">
<div class="col-10">
<input
v-model="todo"
type="text"
class="form-control"
name="todo"
id="todo"
placeholder="Enter New Todo"
v-bind:class="{ 'is-invalid': input_errors.length > 0, 'is-valid': input_errors.length == 0 && todo != '' }"
/>
<div class="invalid-feedback">
<span :key="key" v-for="(error,key) in input_errors">{{ error }}</span>
</div>
</div>
<div class="col-2 d-grid gap-2">
<button class="btn btn-primary btn-s float-end" @click="save">Add</button>
</div>
</div>
</div>
<div class="form-group"></div>
</div>
</div>
-->
<div class="row d-flex justify-content-center mt-3" v-if="Clusters.length">
<div class="col-md-6">
<h5 class="mb-3">Liste des paravalanches:</h5>
Trier par:
<select v-model="sortby">
<option value="altitude">Altitude</option>
<option value="length">Long. de paravalanche</option>
<option value="energy_yr">Energie annuelle</option>
<option value="efficiency">Rendement</option>
</select>
<div style="list-style-type: none;">
<li
v-for="cluster in sortedClusters"
:key="cluster.id"
class="row"
style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 24px;
border-radius: 6px;
margin-bottom: 12px;
border: 2px solid hsla(0, 0%, 0%, 0.35);
"
>
<div class="col-8">
<span
v-bind:style="cluster.done ? 'text-decoration:line-through;' : ''"
>📏 {{ parseInt(cluster.length) }} m - {{ cluster.alt }} m<br>
{{ parseInt(cluster.yield) }} kWh/kWp - {{ parseInt(cluster.energy_total/1000) }} MWh/yr
</span>
</div>
<div class="col-2 d-flex justify-content-center">
<span
class="form-check form-switch"
@click="done_todo(cluster)"
style="margin-left:20px;"
> Installer
<input
class="form-check-input"
type="checkbox"
:id="cluster.id"
:value="cluster.id"
:key="cluster.id"
v-model="choosen_installs"
data-onstyle="#1f2023"
/>
</span>
</div>
<div class="col-2 d-flex justify-content-center">
<!--
<button class="btn btn-primary btn-sm" @click="delete_todo(cluster.id)">+ details</button>
-->
<router-link :to="{ path: `/clusters/${cluster.id}`}">
<button class="btn btn-primary btn-sm">+ details</button>
</router-link>
</div>
</li>
</div>
</div>
<div class="col-md-3">
<h4>Statistiques</h4>
(cocher sur "installer" pour ajouter aux statistiques)
<ul>
<li>Longueur totale: {{ Math.round(stats['length']/1000 * 10)/10 }} km</li>
<li>Energie totale: {{ Math.round(stats['energy']/1000) }} MWh/yr</li>
</ul>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
sortby: 'length',
stats_length: 0,
choosen_installs: [],
todo: "",
Clusters: [],
errors: [],
newTodo: {
todo: "",
done: false
},
responseData: null,
input_errors: []
};
},
computed: {
sortedClusters() {
var clusters = this.Clusters.slice();
if(this.sortby == "length"){
return clusters.sort(function(a,b){
return b.length - a.length;
})
}
else if(this.sortby == "altitude") {
return clusters.sort(function(a,b){
return b.alt - a.alt;
})
}
else if(this.sortby == "energy_yr") {
return clusters.sort(function(a,b){
return b.energy_total - a.energy_total;
})
}
else if(this.sortby == "efficiency") {
return clusters.sort(function(a,b){
return b.yield - a.yield;
})
}
else {
return clusters;
}
},
stats() {
let installed = this.Clusters.filter((item) => this.choosen_installs.includes(item.id))
let energy_sum = 0;
let length_sum = 0;
installed.forEach(function (value) {
energy_sum += value.energy_total;
length_sum += value.length;
});
return {
energy: energy_sum,
length: length_sum,
}
},
},
mounted() {
this.getClusters();
},
methods: {
async getClusters() {
try {
const response = await this.$axios.get("/paralist.js");
console.log('YEAH !')
this.Clusters = response.data;
} catch (error) {
this.errors.push(error);
}
},
/*
async save() {
if (this.input_errors.length > 0 || this.todo == '') {
if (this.todo == '' && this.input_errors.length == 0)
this.input_errors.push('ToDo field cannot be left blank')
this.input_errors.forEach((value) => {
this.$notify(value);
});
} else {
try {
this.newTodo.todo = this.todo;
const response = await this.$axios.post("/todos", this.newTodo);
this.responseData = response.data;
} catch (error) {
this.errors.push(error);
}
this.getTodos();
this.$notify("Added Succesfully");
this.todo = "";
}
},
async delete_todo(index) {
try {
const response = await this.$axios.delete("/todos/" + index);
this.responseData = response.data;
} catch (error) {
this.errors.push(error);
}
this.getTodos();
this.$notify("Deleted Succesfully");
},
async done_todo(todo) {
try {
const response = await this.$axios.put("/todos/" + todo.id, { "todo": todo.todo, "done": !todo.done });
this.responseData = response.data;
} catch (error) {
this.errors.push(error);
}
this.getTodos();
this.$notify("Updated Succesfully");
},
*/
},
watch: {
todo(val) {
this.input_errors = [];
if (val == '') {
this.input_errors.push('ToDo field cannot be left blank')
return;
}
if (val.length < 3 || val.length > 40) {
this.input_errors.push('ToDo field be Minimum 6, Maximum 25 characters')
return;
}
}
}
};
</script>

153
ui/src/components/Home.vue Normal file
View file

@ -0,0 +1,153 @@
<template>
<div class="container my-5">
<span v-if="!Rivers.length">
Loading... If nothing happens in a while, please reload the page
</span>
<div class="row d-flex justify-content-center mt-3" v-if="Rivers.length">
<div class="col-md-6">
<h5 class="mb-3">Uphill rivers:</h5>
Sort by:
<select v-model="sortby">
<option value="wayid">Way id</option>
<option value="alt-diff">altitude difference</option>
<option value="confidence">confidence</option>
<option value="length">length</option>
</select>
<div style="list-style-type: none;">
<li v-for="river in sortedRivers" :key="river.id" class="row" style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 24px;
border-radius: 6px;
margin-bottom: 12px;
border: 2px solid hsla(0, 0%, 0%, 0.35);
">
<div class="col-7">
<span v-if="river.name">
{{ river.name }}
</span>
<b>{{ parseInt(river.length) }} m</b>
( {{ river.slope_confidence }})<br>
Start ele.: {{ river.start_ele | round }} m - End ele.: {{ river.end_ele | round }}m<br>
{{ river.wayid }}
</div>
<!--
<div class="col-2 d-flex justify-content-center">
<span class="form-check form-switch" style="margin-left:20px;"> Choisir
<input class="form-check-input" type="checkbox" :id="cluster.id" :value="cluster.id"
:key="cluster.id" v-model="choosen_installs" data-onstyle="#1f2023" />
</span>
</div>
-->
<br>
<hr>
<div class="col-2 d-flex justify-content-center">
<router-link :to="{ path: `/river/${river.wayid}` }">
<button class="btn btn-primary btn-sm">+ details</button>
</router-link>
</div>
<div class="col-2 d-flex justify-content-center">
<button class="btn btn-primary btn-sm" @click="openOnJOSM(river.wayid)">+ JOSM</button>
</div>
<div class="col-2 d-flex justify-content-center">
<a :href="`https://https://www.openstreetmap.org/way/${ river.wayid }`">
<button class="btn btn-primary btn-sm">OSM</button>
</a>
</div>
</li>
</div>
</div>
<!--
<div class="col-md-3">
<h4>Somme des installations choisies</h4>
(cocher sur "installer" pour ajouter aux statistiques)
<ul>
<li>Longueur totale: {{ Math.round(stats['length'] / 1000 * 10) / 10 }} km</li>
<li>Energie totale: {{ Math.round(stats['energy'] / 1000) }} MWh/yr</li>
</ul>
</div>
-->
</div>
</div>
Contact: f@vi-di.fr
</template>
<script>
export default {
data() {
return {
sortby: 'length',
stats_length: 0,
sortedRivers: [],
Rivers: [],
errors: [],
responseData: null,
input_errors: []
};
},
computed: {
sortedRivers() {
var rivers = this.Rivers.slice();
if (this.sortby == "length") {
return rivers.sort(function (a, b) {
return b.length - a.length;
})
}
else if (this.sortby == "confidence") {
return rivers.sort(function (a, b) {
return b.slope_confidence - a.slope_confidence;
})
}
else if (this.sortby == "alt-diff") {
return rivers.sort(function (a, b) {
return Math.abs(b.start_ele - b.end_ele) > Math.abs(a.start_ele - a.end_ele);
})
}
else {
console.log("Unknown sortby value: " + this.sortby)
return rivers;
}
},
},
mounted() {
this.getRivers();
},
methods: {
async getRivers() {
try {
const response = await this.$axios.get("/rivers.json");
console.log('YEAH !')
this.Rivers = response.data;
} catch (error) {
this.errors.push(error);
}
},
async openOnJOSM(wayid) {
console.log('Will open on josm!')
try {
await this.$axios.get("http://localhost:8111/load_object?new_layer=true&objects=w"+wayid);
console.log('YEAH !')
} catch (error) {
this.errors.push(error);
}
},
},
watch: {
todo(val) {
this.input_errors = [];
if (val == '') {
this.input_errors.push('ToDo field cannot be left blank')
return;
}
if (val.length < 3 || val.length > 40) {
this.input_errors.push('ToDo field be Minimum 6, Maximum 25 characters')
return;
}
}
}
};
</script>

124
ui/src/components/River.vue Normal file
View file

@ -0,0 +1,124 @@
<template>
<div class="container my-5">
<h2 class="text-center">River details</h2>
<div class="row d-flex justify-content-center mt-3">
<div class="col-md-6">
<h3>Characteristics
</h3>
<ul>
<li>Est. length: {{ Math.round(river.length) }}m</li>
<li>Altitude moyenne: {{ cluster.alt }} m</li>
<li>Commune: {{cluster.city}}
<span v-if="cluster.city == 'Bellwald'"></span>
</li>
<li>Puissance installable: {{ Math.round(cluster.num_panels * .4) }} kWp</li>
<li>Azimuth: {{Math.round(cluster.azimuth)}}°</li>
<li>Energie annuelle: {{ Math.round(cluster.energy_total) }} MWh</li>
<li>Efficience: {{ Math.round(cluster.yield) }} kWh/kWp</li>
</ul>
<Bar id="my-chart-id" :options="chartOptions" :data="chartData" />
(ceci est une estimation, sans prise en compte de ombrage)
</div>
<div class="col-md-6">
<ol-map :loadTilesWhileAnimating="true" :loadTilesWhileInteracting="true" style="height: 400px">
<ol-view ref="view" :center="center" zoom="13" :projection="projection" />
<ol-tile-layer>
<ol-source-osm />
</ol-tile-layer>
<ol-vector-layer>
<ol-source-vector>
<ol-feature v-for="c in cluster_geom['features']">
<ol-geom-line-string :coordinates="c['geometry']['coordinates']"></ol-geom-line-string>
<ol-style>
<ol-style-stroke color="blue" width="2"></ol-style-stroke>
</ol-style>
</ol-feature>
</ol-source-vector>
</ol-vector-layer>
</ol-map>
</div>
</div>
</div>
</template>
<script>
import { ref, inject } from 'vue';
import { Bar } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
export default {
name: 'BarChart',
components: { Bar },
data() {
const projection = ref('EPSG:3857');
const format = inject('ol-format');
const geoJson = new format.GeoJSON();
return {
projection,
geoJson,
cluster: {},
cluster_geom: [],
data_loaded: false,
chartOptions: {
responsive: true
}
};
},
computed: {
center() {
if (this.data_loaded) {
console.log(this.cluster.centroid);
return this.cluster.centroid
}
return ref([40, 40]);
},
chartData() {
if(this.data_loaded) {
let dats = this.cluster['pv_norm']['outputs']['monthly']['fixed'];
let graphVals = dats.map((d) => d.E_m)
return {
labels: ['Jan', 'Fev', 'Mar', 'Avr', 'Mai', 'Jun', 'Jul', 'Aou', 'Sep', 'Oct', 'Nov', 'Dec'],
datasets: [{
data: graphVals,
label: 'Energie annuelle/kWp',
}],
}
}
return {
labels: [],
datasets: [],
}
}
},
mounted() {
this.getCluster();
},
methods: {
async getCluster() {
try {
let id = this.$route.params.clusterid;
const rgeom = await this.$axios.get(`/cluster-geom-${id}.json`);
this.cluster_geom = rgeom.data;
const r = await this.$axios.get(`/cluster-meta-${id}.json`);
this.cluster = r.data;
this.data_loaded = true;
console.log(rgeom.data)
} catch (error) {
console.log(error)
this.errors.push(error);
}
},
},
};
</script>

View file

@ -0,0 +1,43 @@
<template>
<ol-map
:loadTilesWhileAnimating="true"
:loadTilesWhileInteracting="true"
style="height: 400px"
>
<ol-view
ref="view"
:center="center"
:rotation="rotation"
:zoom="zoom"
:projection="projection"
/>
<ol-tile-layer>
<ol-source-osm />
</ol-tile-layer>
</ol-map>
</template>
<script>
import { ref, inject } from 'vue';
export default {
setup() {
const center = ref([40, 40]);
const projection = ref('EPSG:4326');
const zoom = ref(3);
const rotation = ref(0);
const format = inject('ol-format');
const geoJson = new format.GeoJSON();
return {
center,
projection,
zoom,
rotation,
geoJson,
};
},
};
</script>

32
ui/src/main.ts Normal file
View file

@ -0,0 +1,32 @@
import { createApp } from 'vue';
import VueRouter from './router';
import App from './App.vue';
import OpenLayersMap from 'vue3-openlayers';
import 'vue3-openlayers/dist/vue3-openlayers.css';
import { Bar } from 'vue-chartjs';
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css"
import "bootstrap"
let baseUrl;
if (window.location.hostname === "localhost") {
baseUrl = "./public/"
}
const axiosInstance = axios.create({
baseURL: baseUrl,
//baseURL: "https://parapower.k3s.fr/data/"
});
const app = createApp(App)
app.config.globalProperties.$axios = axiosInstance;
app.use(OpenLayersMap)
app.use(VueRouter)
app.mount('#app');

21
ui/src/router/index.ts Normal file
View file

@ -0,0 +1,21 @@
import { createRouter, createWebHistory } from "vue-router";
import Home from "../components/Home.vue";
import River from "../components/River.vue";
const routerHistory = createWebHistory();
const router = createRouter({
history: routerHistory,
routes: [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/river/:riverid",
name: "River",
component: River,
}
],
});
export default router;

1
ui/src/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

19
ui/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"allowJs": true
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

9
ui/tsconfig.node.json Normal file
View file

@ -0,0 +1,9 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

9
ui/vite.config.ts Normal file
View file

@ -0,0 +1,9 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
}
})