IT2-eksamen 2015

Har nettopp hatt eksamen i Informasjonsteknologi 2. Tema var “Østfold på hjul“,  utrolig nok delvis relatert til mitt “Prosjekt lokalhistorie”.

Oppgave 1

oppgave 1.html
<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="UTF-8">
    <title>Kart – Østfold på hjul</title>
    <style>
        body {
            width: 600px;
            margin: auto;
            font-family: "Helvetica", sans-serif;
        }
        #kart {
            margin: auto;
            display: block;
        }
    </style>
</head>
<body ng-controller="MyController">

<h1>Østfold på hjul – Lær om stedene</h1>
<p>Klikk på en av byene på kartet for å se en flott video om stedet.</p>

<img id="kart" src="../media/stortkartostfold.gif" usemap="#mapMap"/>
<map name="mapMap">
    <area shape="circle"
          ng-click="apneVideo()"
          ng-repeat="sted in steder"
          ng-attr-coords="{{ sted.koordinater }}">
</map>

<i>Merk: dette fungerer best i Google Chrome</i>

<script src="../res/data.js"></script>
<script src="../res/angular.min.js"></script>
<script>

    var myApp = angular.module("myApp", []);
    myApp.controller("MyController", function ($scope)
    {

        $scope.steder = data.steder;

        /**
         * Åpne videovinduet
         */
        $scope.apneVideo = function () {
            window.open("videoViser.html#" +
                    $scope.steder.indexOf(this.sted), this.sted.navn, "width=640,height=480");
        };
    });
</script>
</body>
</html>
videoViser.html
<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Video – Østfold på hjul</title>
    <style>
        * {
            padding: 0;
            margin: 0;
        }
        body {
            background-color: #000;
            color: #fff;
            font-family: "Helvetica", sans-serif;
            overflow: hidden;
        }
        #tittel {
            position: absolute;
        }
        #error {
            position: absolute;
            margin-top: 50px;
        }
    </style>
</head>
<body>
    <h2 id="tittel"></h2>
    <p id="error">
        Ehm, vi har ingen video til dette stedet ennå.
        Du kan dra til stedet og utforske stedet selv, eller komme tilbake hit senere.
    </p>
    <video id="video" autoplay controls></video>
    <script src="../res/data.js"></script>
    <script>

        var video = document.getElementById("video");
        var tittel = document.getElementById("tittel");
        var error = document.getElementById("error");

        window.onresize = function () {
            video.setAttribute("width", window.innerWidth);
            video.setAttribute("height", window.innerHeight);
        };

        window.onresize();

        window.onhashchange = function () {
            var stedId = window.location.hash.substr(1);
            if (isNaN(stedId)) {
                error.style.display = "block";
                video.style.display = "none";
            } else {
                console.log(stedId);
                tittel.innerHTML = data.steder[stedId].navn;
                if (data.steder[stedId].video === undefined) {
                    error.style.display = "block";
                    video.style.display = "none";
                } else {
                    error.style.display = "none";
                    video.style.display = "block";
                    video.setAttribute("src", data.steder[stedId].video);
                    video.play();
                }
            }
        };

        window.onhashchange();
    </script>
</body>
</html>

Oppgave 2

oppgave 2.html
<!DOCTYPE html>
<html ng-app="myApp">
<head lang="en">
    <meta charset="UTF-8">
    <title>Priskalkulator – Østfold på hjul</title>
    <style>
        body {
            font-family: "Helvetica", sans-serif;
        }
        #kalkulator {
            float: left;
        }
        .sted ul {
            list-style: none;
        }
        #kart {
            float: right;
            display: none; /* Litt trangt om plassen */
        }
    </style>
</head>
<body ng-controller="myController">

<div class="content">

    <h1>Østfold på hjul – priskalkulator</h1>

    <div id="kalkulator">

        <label for="selRute">Velg en rute:</label>
        <select name="selRute" id="selRute" ng-model="valgtRute" ng-change="beregnPris()">
            <option ng-repeat="rute in ruter" ng-value="$index">{{rute.navn}}</option>
        </select>

        <!-- Har valgt å legge inn skiller i listen etter steder -->
        <div id="steder">
            <div class="sted" ng-repeat="sted in ruter[valgtRute].steder">
                <h3 class="sted-tittel">{{ steder[sted].navn }}</h3>
                <ul>
                    <li ng-repeat="attraksjon in steder[sted].attraksjoner">
                        <!-- Oppgaven sier ikke noe spesifikt om det, men jeg tolker
                             teksten til at man bare skal bestille for en person. -->
                        <label>
                            <input type="checkbox" ng-checked="valgteAttraksjoner.indexOf(attraksjon) >= 0"
                                   ng-click="endreBestilling(attraksjon)">

                            <b>{{ attraksjoner[attraksjon].navn }}</b>
                            {{ attraksjoner[attraksjon].pris }} kr
                        </label>
                    </li>
                </ul>
            </div>
        </div>

        <h3 id="sum" ng-show="valgtRute != undefined"><b>Sum: </b>{{sumBilletter}} kr</h3>

    </div>

    <!-- Tolker "Kartet skal vises i applikasjonen" til
         at det er snakk om samme kart som i oppgave 1. Derfor: copy/paste -->
    <img id="kart" src="../media/stortkartostfold.gif" usemap="#mapMap"/>
    <map name="mapMap">
        <area shape="circle"
              ng-click="apneVideo()"
              ng-repeat="sted in steder"
              ng-attr-coords="{{ sted.koordinater }}">
    </map>

</div>

<script src="../res/angular.min.js"></script>
<script src="../res/data.js"></script>
<script src="oppgave2.js"></script>
</body>
</html>
oppgave2.js
/**
 * Script til oppgave 2
 */
var myApp = angular.module("myApp", []);
myApp.controller("myController", ['$scope', function ($scope) {

    $scope.ruter = data.ruter;
    $scope.steder = data.steder;
    $scope.attraksjoner = data.attraksjoner;

    $scope.valgtRute = null;
    $scope.valgteAttraksjoner = [];

    $scope.endreBestilling = function (attraksjon) {
        console.log($scope.attraksjoner[attraksjon]);
        var index = $scope.valgteAttraksjoner.indexOf(attraksjon);
        console.log(this);
        if (index >= 0) {
            $scope.valgteAttraksjoner.splice(index, 1);
        } else {
            $scope.valgteAttraksjoner.push(attraksjon);
        }
        $scope.beregnPris();
    };

    $scope.sumBilletter = 0;

    /**
     * Beregn pris på turen, og dobbeltsjekk at de valgte attraksjonene er med i ruten.
     */
    $scope.beregnPris = function () {
        var sum = $scope.ruter[$scope.valgtRute].pris;
        for (var i = 0; i < $scope.valgteAttraksjoner.length; i++) {
            var attraksjonId = $scope.valgteAttraksjoner[i];

            // Dobbeltsjekk at attraksjonen er med i ruten som er valgt
            var attraksjonIRute = false;
            for (var j = 0; j < $scope.ruter[$scope.valgtRute].steder.length; j++) {
                var sted = $scope.steder[ $scope.ruter[$scope.valgtRute].steder[j] ];
                for (var k = 0; k < sted.attraksjoner.length; k++)
                    if (sted.attraksjoner[k] == attraksjonId) {
                        attraksjonIRute = true;
                        break;
                    }
                if (attraksjonIRute) break;
            }
            if (attraksjonIRute)
                sum += $scope.attraksjoner[attraksjonId].pris;
        }
        $scope.sumBilletter = sum;
    };


    /*
     * Tolker "Kartet skal vises i applikasjonen" til at det
     * er snakk om samme kart som i oppgave 1. Derfor: copy/paste
     */
    $scope.apneVideo = function () {
        window.open("../Oppgave 1/videoViser.html#" +
            $scope.steder.indexOf(this.sted), this.sted.navn, "width=640,height=480");
    };

}]);

Oppgave 3a

Formål

For at det skal oppleves som enkelt å planlegge andre turer på sykkel i fylket, ønsker fylkeskommunen å utvikle en applikasjon som hjelper brukeren med å lage egne ruter mellom steder. Brukerne er folk som ønsker å dra på sykkeltur i området, derfor skal det ikke være nødvendig med opplæring i bruk av applikasjonen.

Brukeren velger startsted og hvilke steder han/hun vil innom på turen. Når et sted legges til, vil applikasjonen beregne avstanden man må sykle, samt hvor lang tid brukeren bør beregne (utfra en gjennomsnittfart på 20 km/t).

Use case 1

Bruker vil vite hvor lang tid det tar å sykle fra Halden gjennom Fredrikstad og til Moss.

  1. Bruker velger startsted Halden.
  2. Applikasjonen spør etter neste sted.
  3. Brukeren velger Fredrikstad.
  4. Applikasjonen viser avstand og tid mellom Halden og Fredrikstad, og spør etter neste sted.
  5. Brukeren velger Moss.
  6. Applikasjonen viser avstand og tid mellom Halden, Fredrikstad og Moss, og spør etter neste sted.
  7. Brukeren angir ikke neste sted.
Use case 2

Bruker har lagt inn noen steder, men ønsker å fjerne/endre et sted

  1. Brukeren trykker på stedet han/hun ønsker å endre.
  2. Applikasjonen viser en liste over andre steder, samt et alternativ for å slette stedet.
  3. Brukeren velger nytt sted eller «slett»
  4. Applikasjonen beregner ny avstand og tid.

Teknologi

Applikasjonen skal lages som en webapp, slik at den er tilgjengelig på (nesten) alle enheter. Den skal skrives i HTML5, JavaScript og CSS.

Inndeling

Datastruktur

Applikasjonen bygger på datastrukturen som brukes ellers i «Østfold på hjul»-prosjektet. Fordelen med dette, er enklere vedlikehold samt at den senere kan integreres i andre deler av applikasjonen.

Merk at avstanden fra og til to steder lagres separat, i tilfelle enkelte strekninger bare kan brukes i en retning (f.eks. Ved enveiskjøring).

ValgteSteder – En liste med ID-nummeret til steder brukeren har valgt, i den rekkefølgen de skal besøkes. Et sted kan være med i denne listen mer enn en gang.

Sted – Her er kun attributter som er relevant i denne delen av løsningen inkludert.

Navn Type Beskrivelse
Id Number Stedets ID-nummer
Navn String Navn på stedet
Avstander Array<Avstand> En liste over avstander fra dette stedet

Avstand

Navn Type Beskrivelse
StedId Number ID-nummeret til stedet denne avstanden går til
Avstand Number Avstanden mellom stedene i kilometer

GUI

Applikasjonen består av to select-bokser og en liste.

Den første select-boksen lar brukeren velge et sted som skal legges til. I applikasjonen skal denne vises under listen.

Listen viser hvilke steder brukeren har valgt så langt. Den er skjult når ingen steder er valgt.

Den andre select-boksen dukker opp når brukeren trykker på et av stedene i listen. Da viser select-boksen en oversikt over andre steder, samt et alternativ for å slette dette stedet fra listen.

Oppgave 3b

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title>Rutemaksinen – Østfold på hjul</title>
    <style>
        * {
            font-family: Helvetica, sans-serif;
        }
        
        #stedListe .sted {
            border-left: 3px solid #333;
            padding: 15px;
        }
        #stedListe .sted:first-child {
            border-top-left-radius: 5px;
        }
        #stedListe .sted:last-child {
            border-bottom-left-radius: 5px;
        }

        #formNyttSted, #sum {
            margin: 10px;
        }
    </style>
</head>
<body>

<h1>Østfold på hjul – Rutemaskinen</h1>

<div id="stedListe">

</div>

<div id="sum">
    <b>Distanse: </b> <span id="sumAvstand"></span> km
    <b>Tid: </b> <span id="sumTid"></span> timer
</div>

<form id="formNyttSted">
    <label for="inpNyttSted">Velg sted:</label>
    <select id="inpNyttSted">
    </select>
    <button type="submit">Legg til</button>
</form>

<script src="../res/data.js"></script>
<script>

    var divSteder = document.getElementById("stedListe");
    var formNyttSted = document.getElementById("formNyttSted");
    var inpNyttSted = document.getElementById("inpNyttSted");
    var dspSumAvstand = document.getElementById("sumAvstand");
    var dspSumTid = document.getElementById("sumTid");

    /**
     * En liste med ID-nummeret til steder brukeren har valgt, i den rekkefølgen de skal besøkes.
     * Et sted kan være med i denne listen mer enn en gang.
     */
    valgteSteder = [];

    // Fyll ut listen over steder
    for (var i = 0; i < data.steder.length; i++) {
        var option = document.createElement("option");
        option.value = i;
        option.innerHTML = data.steder[i].navn;
        inpNyttSted.appendChild(option);
    }

    /**
     * Legg til et sted fra inpNyttSted. Sjekker også at stedet ikke er det samme som det forrige.
     */
    formNyttSted.onsubmit = function (e) {
        e.preventDefault();
        // Stopp om vi ikke fikk inn et tall
        if (isNaN(inpNyttSted.value))
            return;
        // Sjekk at dette ikke er det samme stedet som det forrige
        if (valgteSteder.length > 0 &&
            valgteSteder.lastIndexOf(inpNyttSted.value) == valgteSteder.length - 1)
        {
            alert("Du kan ikke resie fra og til det samme stedet!");
            return;
        }
        // Legg til stedet
        valgteSteder.push(inpNyttSted.value);
        leggTilIListe(valgteSteder.length - 1, inpNyttSted.value);
        beregnTid();
        // Nullstill
        inpNyttSted.value = null;
    };

    /**
     * Endre et sted.
     * Hvis brukeren ikke redigerer listen, bytt ut stedsnavnet med en select-boks som lar brukeren velge et annet sted.
     * Hvis brukeren nettopp har valgt et annet sted, endre verdien i valgteSteder til den verdien brukeren valgte.
     */
    var endreSted = function (index) {
        var stedDiv = divSteder.children[index];
        console.log(index, stedDiv);

        if (!stedDiv.classList.contains("redigerer")) {
            stedDiv.classList.add("redigerer");

            stedDiv.innerHTML = "";

            // Lag et select-element der brukren kan velge et annet sted
            var selectElem = document.createElement("select");

            for (var i = 0; i < data.steder.length; i++) {
                var option = document.createElement("option");
                if (valgteSteder[index] == i)
                    option.setAttribute("selected", "true");
                option.value = i;
                option.innerHTML = data.steder[i].navn;
                selectElem.appendChild(option);
            }
            // Legger også inn muligheten til å fjerne stedet
            var deleteOption = document.createElement("option");
            deleteOption.value = -1;
            deleteOption.innerHTML = "<b>Slett</b>";
            selectElem.appendChild(deleteOption);

            // Og legg til select-elementet
            stedDiv.appendChild(selectElem);
        } else {
            var selectElem = stedDiv.children[0];
            // Finn ut hva brukeren valgte
            var valgtVerdi = selectElem.children[selectElem.selectedIndex].getAttribute("value");

            // Sjekk om dette er samme sted som det forrige eller neste
            if (valgteSteder[index+1] == valgtVerdi || valgteSteder[index-1] == valgtVerdi) {
                alert("Du kan ikke reise fra og til samme sted!");
                return;
            }

            // Har brukeren valgt å slette dette stedet?
            if (valgtVerdi == -1) {
                // Slett objektet fra listen
                valgteSteder.splice(index, 1);
            } else {
                // Bytt ut verdien i listen
                valgteSteder[index] = valgtVerdi;
            }

            // Og tegn det hele på nytt
            regenererListe();
        }
    };

    /**
     * Lag listen over steder på nytt
     */
    var regenererListe = function () {
        // Slett hele listen
        divSteder.innerHTML = "";
        // Lag listen på nytt
        for (var i = 0; i < valgteSteder.length; i++) {
            leggTilIListe(i, valgteSteder[i]);
        }
        beregnTid();
    };

    /**
     * Legg til et sted i UI-listen over steder som skal besøkes
     */
    var leggTilIListe = function (index, stedId) {
        var div = document.createElement("div");
        div.setAttribute("id", "sted-" + index);
        div.setAttribute("class", "sted");
        div.setAttribute("onclick", "endreSted(" + index + ")");
        div.innerHTML = data.steder[stedId].navn;
        divSteder.appendChild(div);
    };

    /**
     * Beregn og vis hvor lang tid det vil ta å sykle den valgte ruten
     * @returns {number} Tiden som brukes, i timer
     */
    var beregnTid = function () {
        var sumAvstand = beregnAvstand();
        // Beregn tiden som blir brukt.
        var sumTid = sumAvstand / 20;
        // Rund av til antall timer, og pass på å ikke vise null til brukren.
        dspSumTid.innerHTML = Math.max(1,
                Math.round(sumTid)
        );
        return sumTid;
    };

    /**
     * Beregn og vis avstanden som sykles
     * @returns {number} Avstanden som sykles
     */
    var beregnAvstand = function () {
        if (valgteSteder.length < 2) {
            sumAvstand = 0;
            return 0;
        }
        var sumAvstand = 0;
        for (var i = 0; i < valgteSteder.length - 1; i++) {
            var sted = data.steder[valgteSteder[i]];
            var nesteStedId = valgteSteder[i+1];
            sumAvstand += sted.avstander[nesteStedId];
        }

        dspSumAvstand.innerHTML = sumAvstand.toFixed(1);

        return sumAvstand;
    };
</script>
</body>
</html>

Til slutt: data.js

/**
 * Inneholder data om steder, ruter og attraksjoner
 */

var data = {
    ruter: [
        {
            navn: "Kort rute",
            pris: 3200,
            steder: [0, 1, 2]
        },
        {
            navn: "Lang rute",
            pris: 7800,
            steder: [0, 4, 3, 1, 2]
        }
    ],

    steder: [
        {
            navn: "Askim", // 0
            attraksjoner: [0],
            koordinater: "261,158,30",
            avstander: {
                3: 62.3,
                1: 58.8,
                2: 44.6,
                4: 33.2
            }
        },
        {
            navn: "Fredrikstad", // 1
            attraksjoner: [1, 2, 3],
            koordinater: "163,447,30",
            video: "../media/fredrikstad.mp4",
            avstander: {
                0: 58.8,
                3: 37.9,
                2: 40.5,
                4: 78.5
            }
        },
        {
            navn: "Moss", // 2
            attraksjoner: [4, 5],
            koordinater: "60,255,30",
            avstander: {
                0: 44.6,
                3: 65.3,
                1: 40.5,
                4: 77.6
            }
        },
        {
            navn: "Halden", // 3
            attraksjoner: [6, 7, 9],
            koordinater: "335,529,30",
            video: "../media/halden_redigert.mp4",
            avstander: {
                0: 62.3,
                1: 37.9,
                2: 65.3,
                4: 60.1
            }
        },
        {
            navn: "Ørje", // 4
            attraksjoner: [8],
            koordinater: "1,1,1", // TODO: Finn Ørje på kartet
            avstander: {
                0: 33.2,
                3: 60.1,
                1: 78.5,
                2: 77.6
            }
        }
    ],

    attraksjoner: [
        {
            navn: "Saftpressekurs Askim saftpresseri", // 0
            pris: 190
        },
        {
            navn: "Skattejakt i Gamlebyen", // 1
            pris: 95
        },
        {
            navn: "Elvelangs Glomma", // 2
            pris: 65
        },
        {
            navn: "Mini-cruise på Glomma", // 3
            pris: 145
        },
        {
            navn: "Våler Klatrepark", // 4
            pris: 195
        },
        {
            navn: "Pilegrimsveien", // 5
            pris: 60
        },
        {
            navn: "Spasertur langs Olavsleden", // 6
            pris: 150
        },
        {
            navn: "Omvisning Rød Herregård", // 7
            pris: 95
        },
        {
            navn: "Rammeverksted på Galleri Lund", // 8
            pris: 120
        },
        {
            navn: "Tur med MS Strømfoss", // 9
            pris: 110
        }
    ]
};