<|start_filename|>arcgis/css/style.css<|end_filename|>
html, body {
    padding: 0;
    margin: 0;
    top: 50px;
    height: calc(100% - 50px);
    width: 100%;
    font-family: Arial;
  }


#header{
  position: absolute;
  top: 0px;
  height: 40px;
  font-size: 20px;
  padding-top: 10px;
  background: white;
  width: 100%;
  background: #034e7b
}

#headerTitle{
  position: absolute;
  padding-left: 20px;
  padding-top: 5px;
  background: #034e7b;
  color: white
}

#viewDiv {
  margin-top: 50px;
  width: 100%;
  height: calc(100% - 50px);
}

#timeline{
  position: absolute;
    bottom: 0px;
    height: 100px;
    background: #034e7b;
    color: white;
    width: 100%;
}

#soft{
    width: 90%;
    left: 100px;
    top: 30px;
    height: 8px;
    background-color: #fda606;
    border-radius: 0px;
}

#timeline-count{
     position: absolute;
    bottom: 125px;
    font-size: 60pt;
    color: #034e7b;
    left: 2%;
    z-index: 99;
}

/* play button 
source: https://stackoverflow.com/questions/28639142/css-creating-a-play-button */

figure {
position: absolute;
    display: -ms-flex;
    display: -webkit-flex;
    display: flex;
    bottom: 10px;
    left: -30px;
}

/* play button */

figure button[name="play"] {
  width: 50px;
  height: 50px;
  background: #fda606;
  border: none;
  border-radius: 100%;
  margin: auto;
  cursor: pointer;
}

figure button[name="play"]:focus {
  outline: 0;
  border: 1px solid hsl(210, 58%, 69%);
  box-shadow: 0 0 0 3px hsla(210, 76%, 57%, 0.5);
}

figure button[name="play"]::after {
  content: '';
  display: inline-block;
  position: relative;
  top: 1px;
  left: 3px;
  border-style: solid;
  border-width: 10px 0 10px 20px;
  border-color: transparent transparent transparent white;
}

/* stop button */

figure button[name="stop"] {
  display: none;
  width: 50px;
  height: 50px;
  background: #fda606;
  border: none;
  border-radius: 100%;
  margin: auto;
  cursor: pointer;
}

figure button[name="stop"]:focus {
  outline: 0;
  border: 1px solid hsl(210, 58%, 69%);
  box-shadow: 0 0 0 3px hsla(210, 76%, 57%, 0.5);
}

figure button[name="stop"]::after {
    content: '';
    display: inline-block;
    position: relative;
    top: 1px;
    left: 0px;
    border-style: solid;
    border-width: 10px 0 10px 20px;
    border-color: white white white white;
}

/* override noUIslider */

.noUi-value-horizontal{
  top: 40%;
  color: white;
  font-size: 9pt
}

.noUi-pips-horizontal{
  height: 60px;
  top: -3px;
}

.noUi-horizontal .noUi-handle{
    width: 16px;
    left: -9px;
    top: -11px;
    background-color: #fda606;
}

.noUi-handle{
    border: 1px solid white;
    border-radius: 0px;
    background: #FFF;
    box-shadow: none;
}

.noUi-handle:after{
  content: none;
} 

.noUi-handle:before{
  content: none;
}

<|start_filename|>arcgis/js/app.js<|end_filename|>
// Sample Application with ArcGIS API for JavaScript 4.5
// Settlement development of the city of Zurich in 3D dynamically animated over time
// Author: <NAME>
// Project: Cartography Lab 2017
// Date: June-September 2017
// Cartography Lab - ETH Zurich

define([
    "esri/core/declare",
    "esri/config",

    "esri/Map",
    "esri/views/SceneView",
    "esri/layers/FeatureLayer",
    "esri/layers/SceneLayer",
    "esri/Basemap",
    "esri/widgets/Home",

    "esri/layers/ElevationLayer",
    "esri/core/urlUtils",

    "esri/symbols/PolygonSymbol3D",
    "esri/symbols/ExtrudeSymbol3DLayer",
    "esri/renderers/SimpleRenderer",
    "esri/symbols/MeshSymbol3D",
    "esri/symbols/FillSymbol3DLayer",

    "esri/widgets/Legend",

    "dojo/dom",
    "dojo/on",
    "dojo/dom-construct",
    "dojo/_base/window",
    "dojo/dom-style",

    "esri/widgets/Search"
], function (
    declare, esriConfig,
    Map, SceneView, FeatureLayer, SceneLayer, Basemap, Home,
    ElevationLayer, urlUtils,
    PolygonSymbol3D, ExtrudeSymbol3DLayer, SimpleRenderer,
    MeshSymbol3D, FillSymbol3DLayer, Legend,
    dom, on, domCtr, win, domStyle,
    Search) {


        return declare(null, {

            constructor: function () {

            },

            init: function () {
				
				// TODO: add service URL of your own building layer
				this.serviceURL = "https://tiles.arcgis.com/tiles/...";
				// TODO: Add building year attributes
				this.yearAttribute = "...";

                // load a new web scene
                var scene = new Map({
                    basemap: "gray",
                    ground: "world-elevation"
                });

                // create a view
                var view = new SceneView({
                    container: "viewDiv",
                    map: scene,
                    qualityProfile: "high"
                });

                // environment settings for better visuals (shadows)
                view.environment.lighting.ambientOcclusionEnabled = true;
                view.environment.lighting.directShadowsEnabled = true;

                window.view = view; // for debugging only
				
				// renderer for already built buildings
                var rendererYearSL = new SimpleRenderer({
                    symbol: new MeshSymbol3D({
                        symbolLayers: [new FillSymbol3DLayer({
                            material: { color: "orange" }
                        })]
                    })
                })

				// renderer for buildings built at year x
                var rendererYearSL2 = new SimpleRenderer({
                    symbol: new MeshSymbol3D({
                        symbolLayers: [new FillSymbol3DLayer({
                            material: { color: "#034e7b" }
                        })]
                    })
                })			

                // create a scene layer with all other buildings (92%)
                this.buildingsLayer = this.createSceneLayer(
                    this.serviceURL,
                    1, rendererYearSL, true
                )
                scene.add(this.buildingsLayer);

                // create a second scene layer with all other buildings that is shown with the buildings of the selected year
                this.buildingsLayer2 = this.createSceneLayer(
					this.serviceURL,
                    1, rendererYearSL2, true
                )
                scene.add(this.buildingsLayer2);

                // create home widget for scene view
                var homeWidget = new Home({
                    view: view
                });
                view.ui.add(homeWidget, "top-left");

                // wait until view is loaded
                view.then(function () {

                    // add legend
                    var legend = new Legend({
                        view: view,
                        layerInfos: [{
                            layer: this.buildingsLayer,
                            title: "Existing buildings at year x"
                        }, {
                            layer: this.buildingsLayer2,
                            title: "Buildings in construction at year x"
                        }]
                    });

                    view.ui.add(legend, "bottom-right");

                    // add timeline slider (noUISlider: https://refreshless.com/nouislider/)
                    var softSlider = document.getElementById('soft');

                    noUiSlider.create(softSlider, {
                        start: 1850,
                        connect: "lower",
                        range: {
                            min: 1850,
                            max: 2030
                        },
                        pips: {
                            mode: 'values',
                            values: [1850, 1900, 1920, 1940, 1960, 1980, 1990, 2000, 2005, 2010, 2015, 2030],
                            density: 50
                        },
                        format: wNumb({
                            decimals: 0
                        })
                    });

                    // trigger timeline animation for first year
                    this.timelineAnimation(1850);

                    // start timeline animation when user interacts with slider
                    softSlider.noUiSlider.on('change', function (values, handle) {
                        this.timelineAnimation(parseInt(values[0]));
                    }.bind(this));

                    // zoom to Zurich
					// TODO: zoom to another location (retrieve view.camera from console in web browser)
                    view.goTo({
                        "position": {
                            "x": 951133.1491238967,
                            "y": 5999621.434980976,
                            "z": 1030.1037597507238,
                            "spatialReference": {
                                "latestWkid": 3857,
                                "wkid": 102100
                            }
                        },
                        "heading": 5.314619878612064,
                        "tilt": 74.10553287910521
                    }, {
                            speedFactor: 5, // animation is 5 times slower than default
                            easing: "linear" // easing function to slow down when reaching the target
                        }
                    );

                    // timeline animation
                    var buttonPlay = dom.byId("button-play");
                    var buttonStop = dom.byId("button-stop");

                    // start animation
                    on(buttonPlay, "click", function () {
                        // change UI of button
                        domStyle.set(buttonPlay, "display", "none");
                        domStyle.set(buttonStop, "display", "inline-block");

                        var year = this.currentYear + 1;

                        this.timelineInterval = setInterval(function () {
                            // cancel Interval
                            softSlider.noUiSlider.set(year);
                            this.timelineAnimation(year); // trigger timeline animation
                            year += 1; // increase year
                            if (year === 2031) { year = 1850; } // make loop
                        }.bind(this), 2000);

                    }.bind(this));

                    // stop animation
                    on(buttonStop, "click", function () {
                        // change UI of button
                        domStyle.set(buttonPlay, "display", "inline-block");
                        domStyle.set(buttonStop, "display", "none");

                        clearInterval(this.timelineInterval);

                    }.bind(this));


                }.bind(this)).otherwise(function (err) {
                    console.error(err);
                });

            },

            timelineAnimation: function (year) {
                // animate buildings (show, color) based on current year on timeline
                this.currentYear = year;

                // change displayed year in the UI
                dom.byId("timeline-count").innerHTML = year;

                // define conditions for buildings based on selected year

                this.buildingsLayer.definitionExpression = this.yearAttribute + " IS NOT null AND " + this.yearAttribute + " > 0 AND " + this.yearAttribute + " < " + year + ""; // buildings that are already built in that year
                this.buildingsLayer2.definitionExpression = this.yearAttribute + " IS NOT null AND " + this.yearAttribute + " > 0 AND " + this.yearAttribute + " = " + year + ""; // buildings that have just been built in that year


            },

            createSceneLayer: function (url, opacity, renderer, visible) {
                // construct a scene layer based an input parameters
                return new SceneLayer({
                    url: url,
                    opacity: opacity,
                    renderer: renderer,
                    visible: visible,
                    elevationInfo: "on-the-ground",
                    popupEnabled: false
                })
            }
        });
    });


<|start_filename|>arcgis/package.json<|end_filename|>
{
	"name": "3D-settlement-development-arcgis",
	"version": "1.0.0",
	"description": "3D Settlement Development with ArcGIS API for JavaScript",
	"dependencies": {
		"nouislider ": "10.1.0",
		"wnumb": "1.1.0"
	},
	"repository": {
		"type": "git"
	},
	"main": "index.html",
	"keywords": [
		"3D city model", "animation"
	],
	"author": "<NAME>",
	"license": "Apache-2.0"
}

<|start_filename|>cesium/js/app.js<|end_filename|>
// Sample Application with Cesium.js
// Settlement development of the city of Zurich in 3D dynamically animated over time
// Author: <NAME>
// Project: Cartography Lab 2017
// Date: June-September 2017
// Cartography Lab - ETH Zurich

// setup a new Cesium viewer
var viewer = new Cesium.Viewer('cesiumContainer', {
	timeline: false, // no timeline (constructed manually)
	animation: false, // no animation
	baseLayerPicker: false, // Hide the base layer picker
	orderIndependentTranslucency: true,
	selectionIndicator: false, // no picking
	infoBox: false // no popup window
});

// short cuts
var scene = viewer.scene;
var camera = scene.camera;

// change scene visualization (make brighter)
scene.skyAtmosphere.brightnessShift = 0.2;
scene.globe.enableLighting = true;

// scene has no terrain due to the fact that 3D tiles do not have z-values
// however, to make animation so that buildings are first below ground not visible
// the depth test needs to be added
scene.globe.depthTestAgainstTerrain = true;

// fly to a predefined position
// TODO: change properties to zoom into your own dataset
camera.flyTo({
	destination: Cesium.Cartesian3.fromDegrees(8.544445, 47.349756, 800),
	orientation: {
		heading: 0.1098761258455383,
		pitch: -0.3341518649803201,
		roll: 0.0003890833756337031
	}
});

// load 3D tiles for existing buildings
// TODO: change tilename with the name of your tileset, copy your tileset in folder 3d-tiles
var tileset = new Cesium.Cesium3DTileset({
	url: './3d-tiles/tilename'
});
// add tileset to scene
scene.primitives.add(tileset);

// define initiale styling for tileset
tileset.readyPromise.then(function (tileset) {
	var boundingSphere = tileset.boundingSphere;
	var range = Math.max(100.0 - boundingSphere.radius, 0.0);
	viewer.camera.viewBoundingSphere(boundingSphere, new Cesium.HeadingPitchRange(0, -2.0, range));
	viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

	var properties = tileset.properties;

	// adjust initiale height off all buildings
	changeAllHeight(tileset, -48);

	// set initiale color and show properties
	// TODO: replace BAUJAHR everywhere with the respective year attribute in your dataset
	if (Cesium.defined(properties) && Cesium.defined(properties.BAUJAHR)) {
		tileset.style = new Cesium.Cesium3DTileStyle({
			color:
			{
				conditions: [
					["${BAUJAHR} === null", "rgba(255,255,255, 0.5)"], // grey
					["true", "rgb(253,166,6)"] // orange
				]
			},
			show: {
				conditions: [
					["${BAUJAHR} === null", 'true'],
					['${BAUJAHR} > 1850', 'false'],
					['true', 'true']
				]
			}
		});
	}

	// trigger timelineAnimation for current year at start up
	timelineAnimation(1850);

}).otherwise(function (error) {
	throw (error);
});

// add 3D tiles for animated buildings
var tilesetAnimation = new Cesium.Cesium3DTileset({
	url: './3d-tiles/buildings_footprints'
});
// add tileset to scene
scene.primitives.add(tilesetAnimation);

// define initiale styling for tileset
tilesetAnimation.readyPromise.then(function (tilesetAnimation) {
	var boundingSphere = tilesetAnimation.boundingSphere;
	var range = Math.max(100.0 - boundingSphere.radius, 0.0);
	viewer.camera.viewBoundingSphere(boundingSphere, new Cesium.HeadingPitchRange(0, -2.0, range));
	viewer.camera.lookAtTransform(Cesium.Matrix4.IDENTITY);

	var properties = tilesetAnimation.properties;

	// adjust initiale height off all buildings
	changeAllHeight(tilesetAnimation, -48);

	// set initiale color and show property
	// TODO: replace BAUJAHR everywhere with the respective year attribute in your dataset
	if (Cesium.defined(properties) && Cesium.defined(properties.BAUJAHR)) {
		tilesetAnimation.style = new Cesium.Cesium3DTileStyle({
			color: "rgb(3,78,123)",
			show: {
				conditions: [
					["${BAUJAHR} === null", 'false'],
					['${BAUJAHR} >= 1850', 'false']
				]
			}
		});
	}

}).otherwise(function (error) {
	throw (error);
});

// global variable height interval
var heightInterval;

// function to adjust height of all buildings (see: https://cesiumjs.org/Cesium/Apps/Sandcastle/gallery/3D%20Tiles%20Adjust%20Height.html)
function changeAllHeight(currenttileset, height) {
	height = Number(height);
	if (isNaN(height)) {
		return;
	}

	var cartographic = Cesium.Cartographic.fromCartesian(currenttileset.boundingSphere.center); // find cartographic coordinates of tileset (center)
	var surface = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, 0.0); // find surface (on height = 0)
	var offset = Cesium.Cartesian3.fromRadians(cartographic.longitude, cartographic.latitude, height); // find offset to surface (delta h)
	var translation = Cesium.Cartesian3.subtract(offset, surface, new Cesium.Cartesian3()); // compute translatation from surface to new height
	currenttileset.modelMatrix = Cesium.Matrix4.fromTranslation(translation); // perform translation on the whole tileset
}

// add timeline slider (noUISlider: https://refreshless.com/nouislider/)
var softSlider = document.getElementById('soft');

noUiSlider.create(softSlider, {
	start: 1850,
	connect: "lower",
	range: {
		min: 1850,
		max: 2030
	},
	pips: {
		mode: 'values',
		values: [1850, 1900, 1920, 1940, 1960, 1980, 1990, 2000, 2005, 2010, 2015, 2030],
		density: 50
	},
	format: wNumb({
		decimals: 0
	})
});

// start timeline animation when user interacts with slider
softSlider.noUiSlider.on('change', function (values, handle) {
	timelineAnimation(values[0]);
});

/* add WMS layer with Cesiums default proxy (only works when node js server is running, start with "npm run start")
var wmsLayer = new Cesium.WebMapServiceImageryProvider({
	url: 'http://www.gis.stadt-zuerich.ch/maps/services/wms/WMS-ZH-STZH-OGD/MapServer/WMSServer',
	parameters: {
		transparent: 'false',
		format: 'image/png'
	},
	layers: 'Uebersichtsplan_2016',
	proxy: new Cesium.DefaultProxy('/proxy/')
});
// add wms layer to viewer
viewer.imageryLayers.addImageryProvider(wmsLayer);*/

// timeline animation
var buttonPlay = document.getElementById("button-play");
var buttonStop = document.getElementById("button-stop");

// start animation
buttonPlay.addEventListener("click", function () {
	// change UI of button
	buttonPlay.style.display = "none";
	buttonStop.style.display = "inline-block";

	var year = this.currentYear + 1;

	this.timelineInterval = setInterval(function () {
		// cancel Interval
		softSlider.noUiSlider.set(year);
		timelineAnimation(year); // trigger timeline animation
		year += 1; // increase year
		if (year === 2031) { year = 1850; } // make loop
	}.bind(this), 2000);

}.bind(this));

// stop animation
buttonStop.addEventListener("click", function () {
	// change UI of button
	buttonPlay.style.display = "inline-block";
	buttonStop.style.display = "none";

	clearInterval(this.timelineInterval);

}.bind(this));

function timelineAnimation(year) {
	// animate buildings (show, color) based on current year on timeline and animate height of buildings
	this.currentYear = parseInt(year); // convert to int incase it's a string

	// adjust the height of animate buildings so they are below ground to start animation
	changeAllHeight(tilesetAnimation, -175);

	// change displayed year in the UI
	document.getElementById("timeline-count").innerHTML = year;

		// built condition for buildings that have not been built yet
		// TODO: replace BAUJAHR everywhere with the respective year attribute in your dataset
		var condition = "${BAUJAHR} >= " + this.currentYear + "";

		if (Cesium.defined(tileset.properties) && Cesium.defined(tileset.properties.BAUJAHR)) {
			tileset.style = new Cesium.Cesium3DTileStyle({
				color:
				{
					conditions: [
						["${BAUJAHR} === null", "rgba(255,255,255, 0.5)"], // grey
						["true", "rgb(253,166,6)"] // orange
					]
				},
				show: {
					conditions: [
						["${BAUJAHR} === null", 'true'],
						[condition, 'false'], // do not show buildings that have not been built yet
						['true', 'true']
					]
				}
			});
		}

		// show the buildings from the current year and animate their height (growing out of the ground)
		if (tilesetAnimation !== undefined) {

			var conditionExact = "${BAUJAHR} === " + this.currentYear + ""; // animate
			var conditionMin = "${BAUJAHR} < " + this.currentYear + ""; // do not show in this tileset for animation
			var conditionMax = "${BAUJAHR} > " + this.currentYear + ""; // do not show in this tileset for animation

			if (Cesium.defined(tilesetAnimation.properties) && Cesium.defined(tilesetAnimation.properties.BAUJAHR)) {
				tilesetAnimation.style = new Cesium.Cesium3DTileStyle({
					color: "rgb(3,78,123)", // blue
					show: {
						conditions: [
							["${BAUJAHR} === null", 'false'],
							[conditionExact, 'true'],
							[conditionMin, 'false'],
							[conditionMax, 'false'],
						]
					}
				});
			}
			// trigger height animation
			animateHeight();
			
		}

	
}

function animateHeight() {
	if (heightInterval !== undefined){
		clearInterval(heightInterval);
	}

	// animate buildings to grow out of the ground
	var height = -100; // starting point at an average height of 52m

	// start interval
	heightInterval = setInterval(function () {
		// trigger change height function
		changeAllHeight(tilesetAnimation, height);
		if (height >= -48) { // building reaches ground
			clearInterval(heightInterval);
			return
		} else {
			height += 1; // increase height by 1m
		}
	}.bind(this), 20);

	return heightInterval;

}