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
-
+