diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80f5d77..fe25674 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,5 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + name: CI on: @@ -38,6 +40,8 @@ jobs: path: 'dist' deploy: + if: ${{ github.ref == 'refs/heads/master' }} + needs: build permissions: diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..07b3d60 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["redhat.vscode-yaml", "runem.lit-plugin"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 473c7e5..692bc9f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,6 @@ { - "files.associations": {"*.json": "jsonc"} + "files.associations": { + "*.json": "jsonc" + }, + "lit-plugin.strict": true } diff --git a/README.md b/README.md index 9c7ac98..5ed9bc6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ The "modern" touch comes from today's advances of the technology (CSS vars, type We provide web applications customized using a json file, CSS variables, graphic resources. These are focused on a particular use-case. -- [Digital twins: building permits](./src/apps/buildings/README.md) [[demo]](https://geoblocks.github.io/ngv/src/apps/buildings/index.html) +- [Digital twins: building permits](./src/apps/permits/README.md) [[demo]](https://geoblocks.github.io/ngv/src/apps/permits/index.html) +- [Digital twins: illumination / solar](./src/apps/illumination/README.md) [[demo]](https://geoblocks.github.io/ngv/src/apps/illumination/index.html) # Application builder diff --git a/index.html b/index.html index 4ce0edc..e12493b 100644 --- a/index.html +++ b/index.html @@ -11,10 +11,15 @@

List of turn-key apps:

diff --git a/public/Assets b/public/Assets new file mode 120000 index 0000000..9140cb0 --- /dev/null +++ b/public/Assets @@ -0,0 +1 @@ +../node_modules/@cesium/engine/Source/Assets/ \ No newline at end of file diff --git a/public/Workers b/public/Workers new file mode 120000 index 0000000..b177ccf --- /dev/null +++ b/public/Workers @@ -0,0 +1 @@ +../node_modules/@cesium/engine/Build/Workers/ \ No newline at end of file diff --git a/src/apps/illumination/README.md b/src/apps/illumination/README.md new file mode 100644 index 0000000..8191012 --- /dev/null +++ b/src/apps/illumination/README.md @@ -0,0 +1,10 @@ +# Digital twins: illumination / solar + +Municipalities: a common use-case for digital twins is to see how shadows are cast by buildings, relief and vegetation. + +- Display a base 3D tileset with buildings +- Position the sun according to some date +- Be able to easily change day / time +- Color roofs / walls? depending on angle with the sun. + - does it make sense? (instantaneous and does not take into account the shades) + - require the information to be part of the 3d-tiles? diff --git a/src/apps/illumination/defaultConfig.json b/src/apps/illumination/defaultConfig.json new file mode 100644 index 0000000..70f31e6 --- /dev/null +++ b/src/apps/illumination/defaultConfig.json @@ -0,0 +1,32 @@ +{ + "header": { + "languages": ["de", "fr", "en", "it"], + "title": { + "fr": "Ma super app", + "en": "My super app", + "de": "Meine supper app", + "it": "Mia super app" + } + }, + "footer": { + "contact": "me@example.com", + "impressum": { + "fr": "Bla bla FR impressim", + "en": "Bla bla EN impressim", + "de": "Bla bla DE impressim", + "it": "Bla bla IT impressim" + } + }, + "app": { + "terrain": "https://3d.geo.admin.ch/ch.swisstopo.terrain.3d/v1/", + "buildings": "https://vectortiles0.geo.admin.ch/3d-tiles/ch.swisstopo.swisstlm3d.3d/20201020/tileset.json", + "vegetation": "https://vectortiles.geo.admin.ch/3d-tiles/ch.swisstopo.vegetation.3d/20190313/tileset.json", + "initialView": { + "destination": [6.628484, 46.5, 1000], + "orientation": { + "heading": 0, + "pitch": -30.0 + } + } + } +} diff --git a/src/apps/illumination/index.html b/src/apps/illumination/index.html new file mode 100644 index 0000000..2d90563 --- /dev/null +++ b/src/apps/illumination/index.html @@ -0,0 +1,15 @@ + + + + + + + Revolutionary illumination / solar app + + + + + + + + diff --git a/src/apps/illumination/index.ts b/src/apps/illumination/index.ts new file mode 100644 index 0000000..963e7b3 --- /dev/null +++ b/src/apps/illumination/index.ts @@ -0,0 +1,37 @@ +import {html} from 'lit'; +import {customElement} from 'lit/decorators.js'; + +import '../../structure/ngv-structure-app.js'; + +// // @ts-expect-error ?url parameter is a viteJS specificity +// import logoUrl from "../../logo.svg?url"; +import {localized} from '@lit/localize'; +import {ABaseApp} from '../../structure/BaseApp.js'; + +// @ts-expect-error viteJS specific import +import configUrl from './defaultConfig.json?url'; + +import './ngv-main-illumination.js'; +import {IIlluminationConfig} from './ingv-config-illumination.js'; + +@customElement('ngv-app-illumination') +@localized() +export class NgvAppIllumination extends ABaseApp { + constructor() { + super(configUrl as string); + } + + render() { + const r = super.render(); + if (r) { + return r; + } + return html` + + + + `; + } +} diff --git a/src/apps/illumination/ingv-config-illumination.ts b/src/apps/illumination/ingv-config-illumination.ts new file mode 100644 index 0000000..8d8542b --- /dev/null +++ b/src/apps/illumination/ingv-config-illumination.ts @@ -0,0 +1,16 @@ +import {INgvStructureApp} from '../../structure/ngv-structure-app.js'; + +export interface IIlluminationConfig extends INgvStructureApp { + app: { + terrain: string; + buildings: string; + vegetation: string; + initialView: { + destination: [number, number, number]; + orientation: { + heading: number; + pitch: number; + }; + }; + }; +} diff --git a/src/apps/illumination/ngv-main-illumination.ts b/src/apps/illumination/ngv-main-illumination.ts new file mode 100644 index 0000000..614059d --- /dev/null +++ b/src/apps/illumination/ngv-main-illumination.ts @@ -0,0 +1,242 @@ +import {customElement, property, query, state} from 'lit/decorators.js'; +import {css, html, LitElement, PropertyValues} from 'lit'; +import { + Cartesian3, + Cesium3DTileset, + CesiumTerrainProvider, + CesiumWidget, + JulianDate, + Math as CMath, + ShadowMode, + Terrain, +} from '@cesium/engine'; +import {IIlluminationConfig} from './ingv-config-illumination.js'; + +const YEAR = new Date().getFullYear(); +const BASE_DATE = new Date(`${YEAR}-01-01T00:00:00`); +const BASE_JULIAN_DATE = JulianDate.fromDate(BASE_DATE); + +@customElement('ngv-main-illumination') +export class NgvMainIllumination extends LitElement { + @state() + day: number = 1; + @state() + hour: number = 12; + private viewer: CesiumWidget; + @query('#cesium-container') + cesiumContainer: HTMLDivElement; + @query('.hour-slider') + hourSlider: HTMLInputElement; + @query('.day-slider') + daySlider: HTMLInputElement; + + @property({type: Object}) + config: IIlluminationConfig['app']; + + static styles = css` + .app-container { + margin-top: 10px; + width: 100%; + } + + #cesium-container { + width: 100%; + height: calc(100vh - 320px); + padding: 10px 0; + display: flex; + } + + #cesium-container .cesium-widget, + #cesium-container .cesium-widget canvas { + width: 100%; + height: 100%; + } + + .controls { + position: absolute; + display: flex; + flex-direction: row; + width: 95%; + margin-top: 10px; + padding: 10px; + column-gap: 10px; + background: rgba(0, 0, 0, 0.3); + color: white; + } + + .slider-container { + display: flex; + flex-direction: column; + width: 100%; + } + + .day-slider { + background-image: linear-gradient( + to right, + #1843ef 8.3%, + #96de23 33.3%, + #d2c801 58.3%, + #f8700e 83.3%, + #1843ef 100% + ); + //background-image: linear-gradient(to left, #1893EF 8.3%, #f8700e 8.3% 33.3%, #d2c801 33.3% 58.3%, #96de23 58.3% 83.3%, #1893EF 83.3% 100%) + } + + .hour-slider { + background-image: linear-gradient( + to right, + #000033 0%, + /* 00:00 Night */ #000033 20%, + /* 05:00 */ #003366 25%, + /* 06:00 Dawn */ #6699ff 33%, + /* 09:00 Day */ #ffffcc 66%, + /* 16:00 Peak Daylight */ #ff9966 75%, + /* 17:00 Dusk */ #cc3300 80%, + /* 19:00 */ #000033 100% /* 23:00 Night */ + ); + } + + .slider-container input { + -webkit-appearance: none; + width: 100%; + height: 10px; + } + + .slider-container input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 12px; + height: 12px; + border: 0; + background: red; + cursor: pointer; + } + + .slider-container input::-moz-range-thumb { + width: 12px; + height: 12px; + border: 0; + background: red; + cursor: pointer; + } + `; + + private async initializeViewer() { + window.CESIUM_BASE_URL = '/'; + const { + terrain: terrainUrl, + buildings: buildingsUrl, + vegetation: vegetationUrl, + initialView, + } = this.config; + this.viewer = new CesiumWidget(this.cesiumContainer, { + shadows: true, + scene3DOnly: true, + terrain: new Terrain(CesiumTerrainProvider.fromUrl(terrainUrl)), + terrainShadows: ShadowMode.ENABLED, + }); + const buildingsTS = await Cesium3DTileset.fromUrl(buildingsUrl, { + show: true, + backFaceCulling: false, + }); + this.viewer.scene.primitives.add(buildingsTS); + const vegetationTS = await Cesium3DTileset.fromUrl(vegetationUrl, { + show: true, + backFaceCulling: false, + }); + this.viewer.scene.primitives.add(vegetationTS); + + this.viewer.camera.flyTo({ + destination: Cartesian3.fromDegrees(...initialView.destination), + orientation: { + heading: CMath.toRadians(initialView.orientation.heading), + pitch: CMath.toRadians(initialView.orientation.pitch), + }, + duration: 0, + }); + } + + protected async firstUpdated(_changedProperties: PropertyValues) { + await this.initializeViewer(); + this.updateDayAndHour(); + super.firstUpdated(_changedProperties); + } + + // FIXME: extract slider to own component + + // FIXME: extract Cesium to own component + + protected render() { + return html` +
+ +
+
+ + +
+
+ + +
+
+
+
+ `; + } + + get time() { + BASE_DATE.setHours(this.hour); + return BASE_DATE.toLocaleTimeString(); + } + + get date() { + BASE_DATE.setMonth(0); + BASE_DATE.setDate(this.day); + const day = BASE_DATE.getDate(); + const monthName = BASE_DATE.toLocaleString('default', {month: 'long'}); + return `${day} of ${monthName} (day ${this.day})`; + } + + updateDayAndHour() { + this.hour = parseInt(this.hourSlider.value); + this.day = parseInt(this.daySlider.value); + JulianDate.addHours( + BASE_JULIAN_DATE, + (this.day - 1) * 24 + this.hour, + this.viewer.clock.currentTime, + ); + } + + get daysInYear() { + return (YEAR % 4 === 0 && YEAR % 100 !== 0) || YEAR % 400 === 0 ? 366 : 365; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'ngv-main-illumination': NgvMainIllumination; + } +} + +declare global { + interface Window { + CESIUM_BASE_URL: string; + } +} diff --git a/src/apps/buildings/README.md b/src/apps/permits/README.md similarity index 100% rename from src/apps/buildings/README.md rename to src/apps/permits/README.md diff --git a/src/apps/buildings/defaultConfig.json b/src/apps/permits/defaultConfig.json similarity index 100% rename from src/apps/buildings/defaultConfig.json rename to src/apps/permits/defaultConfig.json diff --git a/src/apps/buildings/index.html b/src/apps/permits/index.html similarity index 80% rename from src/apps/buildings/index.html rename to src/apps/permits/index.html index 27577d3..4cb48a1 100644 --- a/src/apps/buildings/index.html +++ b/src/apps/permits/index.html @@ -4,12 +4,12 @@ - Revolutionary buildings app + Revolutionary building permits app - + diff --git a/src/apps/buildings/index.ts b/src/apps/permits/index.ts similarity index 88% rename from src/apps/buildings/index.ts rename to src/apps/permits/index.ts index f52530e..973a731 100644 --- a/src/apps/buildings/index.ts +++ b/src/apps/permits/index.ts @@ -13,9 +13,9 @@ import {ABaseApp} from '../../structure/BaseApp.js'; import configUrl from './defaultConfig.json?url'; // -@customElement('ngv-app-buildings') +@customElement('ngv-app-permits') @localized() -export class NgvAppBuildings extends ABaseApp { +export class NgvAppPermits extends ABaseApp { constructor() { super(configUrl as string); } diff --git a/vite.config.js b/vite.config.js index 8d541c1..96dda37 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,11 +8,14 @@ export default defineConfig({ strictPort: true, port: 1234, }, + build: { + assetsDir: 'vassets', rollupOptions: { input: { main: resolve(__dirname, 'index.html'), - buildings: resolve(__dirname, 'src/apps/buildings/index.html'), + permits: resolve(__dirname, 'src/apps/permits/index.html'), + illumination: resolve(__dirname, 'src/apps/illumination/index.html'), // other: resolve(__dirname, "src/apps/ngv-something-else.html"), }, },