Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Expand
titleDel 1 – BikeRental-, Bike-, og Location-klassene (35%)

BikeRental-klassen administrerer stasjonene, syklene og utleieforhold. Vi gjør det enkelt og identifiserer stasjonene med et geografisk punkt (GeoLocation, se skjelettet), altså lengde- og breddegrad. GeoLocation brukes også for lagring av posisjonen til syklene.

Oppgave 1 a) - GeoLocation

GeoLocation er kodet slik at instansene ikke kan endres. Hva er fordelen med dette?

Expand
titleLF
  • En stasjon holder et sett med geografiske koordinater. Disse skal ikke endre seg, så det er en god skikk å gjøre dem umulig å endre så en ikke uforvarende gjør endringer på dem.
Oppgave 1b) - GeoLocation

Skriv ferdig double distance(GeoLocation)-metoden i GeoLocation.

Expand
titleLF

Det eneste en skulle implementere her var distance(Geolocation other). Det var fullt mulig å implementere denne uten å bruke noen matematiske ting. Hvis en ser på skjelettet for GeoLocation eksisterer det en metode der allerede - distance(lat1, long1, lat2, long2). Det eneste en må gjøre er å returnere et kall til denne metoden.

Vi legger også ved en implementasjon av denne hjelpemetoden, mens den i skjelettet var tom.

Code Block
public class GeoLocation {

// Slettet gettere og settere etc.

// Kode for 1b
 public double distance(final GeoLocation other) {
  return distance(latitude, longitude, other.latitude, other.longitude);
 }


// Under følger kode for metoden dere kunne forvente at eksisterte.
 /*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 /*::                                                                         :*/
 /*::  This routine calculates the distance between two points (given the     :*/
 /*::  latitude/longitude of those points). It is being used to calculate     :*/
 /*::  the distance between two locations using GeoDataSource (TM) prodducts  :*/
 /*::                                                                         :*/
 /*::  Definitions:                                                           :*/
 /*::    South latitudes are negative, east longitudes are positive           :*/
 /*::                                                                         :*/
 /*::  Passed to function:                                                    :*/
 /*::    lat1, lon1 = Latitude and Longitude of point 1 (in decimal degrees)  :*/
 /*::    lat2, lon2 = Latitude and Longitude of point 2 (in decimal degrees)  :*/
 /*::  Worldwide cities and other features databases with latitude longitude  :*/
 /*::  are available at http://www.geodatasource.com                          :*/
 /*::                                                                         :*/
 /*::  For enquiries, please contact sales@geodatasource.com                  :*/
 /*::                                                                         :*/
 /*::  Official Web site: http://www.geodatasource.com                        :*/
 /*::                                                                         :*/
 /*::           GeoDataSource.com (C) All Rights Reserved 2015                :*/
 /*::                                                                         :*/
 /*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 public static double distance(final double lat1, final double lon1, final double lat2, final double lon2) {
  final double theta = lon1 - lon2;
  double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2)) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
  dist = Math.acos(dist);
  // convert to degrees
  dist = rad2deg(dist);
  dist = dist * 60 * 1.1515;
  // convert to meters
  dist = dist * 1609.344;
  return dist;
 }
 /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 /*:: This function converts decimal degrees to radians       :*/
 /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 private static double deg2rad(final double deg) {
  return (deg * Math.PI / 180.0);
 }
 /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 /*:: This function converts radians to decimal degrees       :*/
 /*:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::*/
 private static double rad2deg(final double rad) {
  return (rad * 180 / Math.PI);
 }
}




Vanlige feil:

 Det viser seg ganske vanlig at folk på eksamen ikke leser hele oppgaven, ei heller hele klassen til en metode som skal implementeres, før en implementerer den. Dette er ikke lurt.

Oppgave 1c) - Bike

Bike-klassen skal ha assosiasjoner (koblinger) til Person- (se skjelettet) og GeoLocation-klassene iht. følgende klassediagram.

(Location i bildet over skulle hett GeoLocation, men det var tydelig nok i forhold til teksten hva som er ment.)

Begge disse assosiasjonene er dynamiske, dvs. både location og renter (låntaker) kan endres. location-koblingen endres (automagisk, du kan bare forholde deg til at location hele tiden oppdateres) når sykkelen beveger seg, mens renter-koblingen endres når leieforhold innledes og avsluttes.

Skriv felt og metoder for location- og renter-assosiasjonene iht. Java sine kodingskonvensjoner.

Expand
titleLF

Bike skal potensielt lagre én lokasjon (GeoLocation) og én Person. Her spør vi kun om å implementere feltene, samt metoden for å sette inn og hente ut verdiene.

Code Block
public class Bike {
 @Override

 private GeoLocation location;
 private Person renter;

 public GeoLocation getLocation() {
  return location;
 }
 void setLocation(final GeoLocation location) {
  this.location = location;
 }
 public Person getRenter() {
  return renter;
 }
 void setRenter(final Person renter) {
  this.renter = renter;
 }


Vanlige feil:

 

Oppgave 1d) - BikeRental

BikeRental må holde oversikt over alle stasjonene, representert med GeoLocation, og alle syklene (Bike). Skriv nødvendige variabel-deklarasjoner (felt) for dette og forklar hva som styrer valget av typer. Du skal ikke skrive metoder for å konfigurere systemet med stasjoner og sykler, men anta at slike finnes.

Expand
titleLF

Her skulle du velge hva slags representasjon du vil ha for alle stasjonene og syklene i systemet. Et slikt valg som kommer på veldig mange eksamener. I LF har vi valgt å bruke en Collection for begge to. Dette fordi vi ikke har noen behov for liste-metoder som get(i) osv.

Du skulle ikke (her) skrive de ulike metodene som manipulerte systemet mhp fordeling av sykler og stasjoner.

Code Block
public class BikeRental {

	private final Collection<GeoLocation> places = new ArrayList<GeoLocation>();
    private final Collection<Bike> allBikes = new ArrayList<Bike>();
	// Forventede metoder:

	void addPlace(final GeoLocation location) {
		places.add(location);
	}

	void addBike(final Bike bike, final GeoLocation location) {
		if (location != null) {
			bike.setLocation(location);
		}
		allBikes.add(bike);
	}


Vanlige feil:

 

Beskrivelse av oppgavene 1e og 1f

I første omgang skal du skrive forenklet kode for leie og retur av sykler, hvor det ikke tas hensyn til annet enn start- og slutt-tidspunkt for leieforholdet. Forlenging av leie ser vi foreløpig bort fra. renter-assosiasjonen i Bike skal holde rede på hvem (Person) som evt. leier en sykkel (Bike). Du bestemmer selv hvor/hvordan du vil representere når leieforholdet starter og når det forventes at sykkelen leveres tilbake. Det viktigste er at informasjonen kan brukes når sykkelen leveres og leien skal beregnes og betales.

Oppgave 1e) - BikeRental

Skriv metodene under i klassen BikeRental (se skjelettet til venstre for detaljene). Disse metodene bygger på representasjonen du valgte i 1d.

  • countAvailableBikesNearby - teller hvor mange ledige sykler det er innen en viss avstand (hjelper en potensiell låner (renter) å finne ut om det er sykler i nærheten)
  • getStationNearby – finner (lokasjonen til) stasjonen som er nærmest en sykkel og innenfor en viss minimumsavstand (hjelper en låner (renter) å finne ut hvor en sykkel kan settes tilbake)
  • getRentedBikes – returnerer alle syklene som er lånt ut
  • getUnreturnedBikes – returnerer alle syklene som er plassert ved en stasjon, men som fortsatt er knyttet til en låner (renter)  (brukes når en skal sende varsler til de som har satt fra seg sykkelen, men glemt å angi den som levert)
Expand
titleLF

Disse oppgavene dreide seg stort sett om filtrering av de ulike strukturene for stasjoner og sykler, og metodekall for å finne distanse osv. Det var oppgaver som kunne løses veldig bra ved hjelp av Streams og lambdaer, vi viser et par eksempler på slike.

Code Block
    private int countAvailableBikesNearby(final GeoLocation location, final double distance) {
        int count = 0;
        for (final Bike bike : allBikes) {
            if (bike.getRenter() == null && bike.getLocation().distance(location) <= distance) {
                count++;
            }
        }
        return count;
        //        return allBikes.stream().filter(bike -> bike.getRenter() == null && bike.getLocation().distance(location) <= distance).count();
    }
    private GeoLocation getStationNearby(final Bike bike, final double maxDistance) {
        GeoLocation closestPlace = null;
        double minDistance = maxDistance;
        for (final GeoLocation place : places) {
            final double distance = bike.getLocation().distance(place);
            if (distance < minDistance) {
                closestPlace = place;
                minDistance = distance;
            }
        }
        return closestPlace;
    }

    private Collection<Bike> getRentedBikes() {
        final Collection<Bike> result = new ArrayList<Bike>();
        for (final Bike bike : allBikes) {
            if (bike.getRenter() != null) {
                result.add(bike);
            }
        }
        return result;
        //    return allBikes.stream().filter(bike -> bike.getRenter() != null).collect(Collectors.toList());
    }

    //    Kan dere skrive den siste uten Streams? 😊
    private Collection<Bike> getUnreturnedBikes() {
        return getRentedBikes().stream().filter(bike -> getStationNearby(bike, 30.0) != null).collect(Collectors.toList());
    }

Vanlige feil:

 

Oppgave 1f) - BikeRental (mer)

Skriv metodene rentBike og returnBike i klassen BikeRental (se skjelettet for detaljene, merk spesielt krav om utløsing av unntak). Disse metodene endrer tilstanden som brukes av de foregående metodene. Velg selv passende unntakstype(r).

LocalDateTime brukes for å representere tid (erstatter Date i moderne Java). Gjør nødvendige antakelser om metoder den har, som trengs for tidsberegninger, og bruk disse antakelsene.


rentBike – kalles når en person starter å leie en gitt sykkel
returnBike – kalles når en person leverer tilbake sykkelen og betaler for leien (i denne oppgaven skal pris være 10 kr. pr. påbegynte time)

Expand
titleDel 1 – BikeRental-, Bike-, og Location-klassene (35%)

Her var det viktig å lese javadokumentasjonen for metodene, og se nøye på hva unntakene skulle sjekke for samt utløse. I løsningsforslaget har vi laget en hjelpemetode checkNowIsBeforeReturnTime somsjekker to LocalDateTime mot hverandre ved hjelp av LDT.compareTo(LDT). Når en Person leier en sykkel må vi huske på å oppdatere dette i sykkelen.

Expand
titleLF

    public void rentBike(final Person person, final Bike bike, final LocalDateTime now, final LocalDateTime returnTime) {

        checkNowIsBeforeReturnTime(now, returnTime);

        if (bike.getRenter() != null) {

            throw new IllegalArgumentException(bike + " is currently being rented");

        }

        bike.setRenter(person);

    }


    private void checkNowIsBeforeReturnTime(final LocalDateTime now, final LocalDateTime returnTime) {

        if (now.compareTo(returnTime) >= 0) {

            throw new IllegalArgumentException("The start time, " + now + " is the same as or after the return time, " + returnTime);

        }

    }

    public void returnBike(final Person person, final Bike bike, final LocalDateTime now) {

        if (bike.getRenter() != person) {

            throw new IllegalArgumentException(bike + " isn't currently being rented by " + person);

        }

        final GeoLocation place = getStationNearby(bike, 30.0);

        if (place == null) {

            throw new IllegalArgumentException(bike + " isn't near (enough) a bike place");

        }

// part 2        person.withdraw(computePrice(person, bike, now)); // We didn't focus on the implementation of withdrawing money.

// part 2        this.printReceipt(person, bike, now);

        bike.setRenter(null);

// part 2        bike.clearRentalInfos();

    }



 



...

Expand
titleDel 2 – Gebyr og kvittering (20%)

BikeRental-klassen skal utvides til å støtte forlengelse av leie, beregning av gebyrer for forlengelse og for å forlenge eller levere tilbake sykkelen etter angitt tid. I tillegg skal det lagres nok data om dette, så en kan få skrevet ut en kvittering med forklaring på hvordan prisen framkom.


Oppgave 2a)

Forklar med tekst og/eller kode hvordan du vil lagre informasjon om evt. forlengelser av leieforholdet, slik at systemet har nok informasjon til å kunne

Expand
titleLF

Hver enkelt sykkel kan leies i flere perioder (start pluss utvidelser), men det må være av den samme personen. Informasjonen om alle disse periodene må lagres en plass. Vi må lagre start- og sluttidspunkt for alle disse periodene. Hvis en person utvider leietiden fem minutter etter at den forrige perioden var gått ut skal det legges på en avgift.

 

All denne informasjonen kan lagres på ulike steder. I LF har vi tatt utgangpunkt i at alle disse tidsperiodene er knyttet til én enkelt sykkel. En sykkel har allerede knyttet én person til seg, den som leier. Det er derfor naturlig å la sykkelobjektet lagre informasjon om alle leieperiodene.

I LF har vi introdusert en ny klasse, RentalInfo. Denne inneholder to LocalDateTime, en for starten av en periode og en for slutten. Bike har en List av slike, og passende gettere/settere.

Code Block
languagejava
linenumberstrue
public class RentalInfo {

	private final LocalDateTime startTime;
	private final LocalDateTime endTime;

	public RentalInfo(final LocalDateTime startTime, final LocalDateTime endTime) {
  		this.startTime = startTime;
		this.endTime = endTime;
	}

	public LocalDateTime getStartTime() {
		return startTime;
	}

	public LocalDateTime getEndTime() {
		return endTime;
	}
}

public class Bike {

	// [part 1 stuff not shown here]

	// for computing rental price

	private final List<RentalInfo> rentals = new ArrayList<RentalInfo>();

	void addRentalInfo(final RentalInfo rentalInfo) {
	rentals.add(rentalInfo);
	}

	List<RentalInfo> getRentalInfos() {
	return new ArrayList<RentalInfo>(rentals);
	}

	void clearRentalInfos() {
	rentals.clear();
	}
}



Vanlige feil: ...

Oppgave 2b)

Gjør nødvendige endringer av rentBike- og returnBike-metodene i BikeRental, iht. de nye kravene nevnt over, og skriv i tillegg extendRental-metoden. Baser deg på en pris på 10 kr. pr. påbegynte time (som over), 5 kr. pr. forlengelse og 10 kr. pr. for sen forlengelse eller tilbakelevering.

Expand
titleLF

I rentBike legges det til en linje som legger inn start-og sluttidspunkt for første leieperiode, i LF i form av en RentalInfo. I extendRental gjøres det tilsvarende.

Den kompliserte biten av dette er returnBike – når en skal returnere sykkelen. Da må en beregne kostnaden av leien, og trekke dette fra kunden på en eller annen måte (som vi ikke har spesifisert, og heller ikke henger oss veldig opp i). Vi har her valgt å trekke ut selve beregningen av kostnad i en egen metode, computePrice.

Code Block
languagejava
linenumberstrue
    // Hjelpefunksjon for å beregne pris. 
    public int computePrice(final Person person, final Bike bike, final LocalDateTime returnTime) {
        final List<RentalInfo> rentalInfos = bike.getRentalInfos();
        int price = 0;
        LocalDateTime lastEndTime = null;

        for (final RentalInfo info : rentalInfos) {
            if (lastEndTime == null) {
                int timer = computeHours(info.getStartTime(), returnTime);
                price = 10 * timer;
            } else if (info.getEndTime().compareTo(lastEndTime) > 0) {
                price += 5;
                if (info.getStartTime().compareTo(lastEndTime) > 0) {
                    price += 10;
                }
            }
            lastEndTime = info.getEndTime();
        }
        if (returnTime.compareTo(lastEndTime) > 0) {
            price += 10;
        }
        return price;
    }

    // Find full hours of rent. Get full hours, add one.
    private int computeHours(final LocalDateTime startTime, final LocalDateTime endTime) {
        return (int) Duration.between(startTime, endTime).toHours() + 1;
    }


    public void rentBike(final Person person, final Bike bike, final LocalDateTime now, final LocalDateTime returnTime) {
        checkNowIsBeforeReturnTime(now, returnTime);
        if (bike.getRenter() != null) {
            throw new IllegalArgumentException(bike + " is currently being rented");
        }
        bike.setRenter(person);
        bike.addRentalInfo(new RentalInfo(now, returnTime)); // NEW
    }


    public void returnBike(final Person person, final Bike bike, final LocalDateTime now) {
        if (bike.getRenter() != person) {
            throw new IllegalArgumentException(bike + " isn't currently being rented by " + person);
        }
        final GeoLocation place = getStationNearby(bike, 30.0);
        if (place == null) {
            throw new IllegalArgumentException(bike + " isn't near (enough) a bike place");
        }
        // We have not specified how money is withdrawn from
// Persons. Next line is example.
        person.withdraw(computePrice(person, bike, now)); // NEW
        bike.setRenter(null);
        bike.clearRentalInfos(); // NEW
    }

    public void extendRental(final Person person, final Bike bike, final LocalDateTime now, final LocalDateTime returnTime) {
        checkNowIsBeforeReturnTime(now, returnTime);
        if (bike.getRenter() != person) {
            throw new IllegalArgumentException(bike + " isn't currently being rented by " + person);
        }
        bike.addRentalInfo(new RentalInfo(now, returnTime));
    }

Vanlige feil: ...

Oppgave 2c)


Skriv metoden printReceipt i BikeRental, som kan kalles av returnBike for å skrive ut kvitteringen iht. formatet gitt nederst i skjelettet til venstre.


Vær obs på at det eksisterer en metode formatLocalDateTime i skjelettet, som kan hjelpe.

Expand
titleLF

Den eksisterende klassen formatLocalDateTime hjelper til med å vise hvordan en kan formattere uthenting av data på HH:mm-format fra LocalDateTime – akkurat det som trengs. Siden Bike inneholder all informasjon om leieforholdet hentes dette ut herfra. Returtidspunkt får en som en parameter. Deretter kan en hente ut informasjon om starttidspunkt og antall utvidelser skrives ut ved å gå gjennom listen. Det første elementet i listen rentalInfos er første leietid, alle de andre er utvidelser.

Vi må i tillegg beregne kostnad. Dette ble gjort i en egen metode, computePrice, i 2b.

Code Block
languagejava
themeEclipse
collapsetrue
	private void printReceipt(Person person, Bike bike, LocalDateTime returnTime) {
        final List<RentalInfo> rentalInfos = bike.getRentalInfos();
        System.out.println("Initial Rental from "+formatRentalInfoTime(rentalInfos.get(0)));
        rentalInfos.remove(0); // Fjerner den første
        for (int i = 0; i < rentalInfos.size(); i++) {
            RentalInfo re = rentalInfos.get(i);
            System.out.println("Extension "+(i+1)+" from "+formatRentalInfoTime(re));
        }
        System.out.println("Bike returned at "+
                returnTime.format(DateTimeFormatter.ofPattern("HH:mm")));
        System.out.println("Total cost: "+computePrice(person, bike, returnTime));
    }

Vanlige feil: ...


...

Expand
title Del 4 – mobil-appen (15%)

Under ser du et utkast til en mobil-app for sykkelutleietjenesten, basert på FXML. Kartet til venstre viser hvor det er sykler, og én stasjon er valgt (og implisitt også en sykkel på stasjonen). Brukeren kan endre antatt tidspunkt for tilbakelevering ved å endre teksten i tekstfeltet eller bruke knappene til høyre for feltet. Den vesentlige (for denne oppgaven) FXML-koden er vist til høyre.

Image Modified

<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="kont2019.bike.BikeRentalController">

   ...

   <TextField fx:id="fromInput"/>

   <TextField fx:id="toInput"/>

   ...

   <Button onAction="#plus1HourAction" text="+ 1 hour"/>

   <Button onAction="#minus1HourAction" text="- 1 hour"/>

   <Button onAction="#rentAction" text="Rent"/>

</VBox>


Oppgave 4a)

Gitt denne FXML-koden, hvilke kode-elementer (klasser, felt og metoder) må finnes i koden som implementerer oppførselen (logikken) til appen?

Expand
titleLF

Av FXML-koden kan en lese at det grafiske grensesnittet (appen) knyttes til BikeRental ved hjelp av klassen BikeRentalController. Dette er i tråd med navnekonvensjonene brukt i tidligere eksamener. Det refereres til to tekstfelt som en i koden kobler seg til med henholdsvis  @FXML private TextField fromInput og toInput. Likeledes må en lage tre metoder (@FXML private void plus1HourAction, minus1HourAction og rentAction) som kalles når knappene i grensesnittet trykkes inn.

Oppgave 4b)

I kodeskjelettet er ufullstendig kode for oppførselen til appen angitt (Kontroller-logikk). Fyll ut de manglende kodeelementene, som angitt i kommentarer eller markert med ???

Det er lov å anta at LocalDateTime-klassen har konstruktør og metode for konvertering fra og til String, samt metoder for å lage nye LocalDateTime-instanser et gitt antall sekunder før og etter.

Expand
titleLF

Her er det (foreløpig) lagt inn mer kode enn de ulike metodene som skulle implementeres i oppgaven.

Code Block
languagejava
public class BikeRentalController {

    private final BikeRental bikeRental;
    private final Person me;
    private final Bike bike;

    public BikeRentalController() {
        bikeRental = new BikeRental();
        final GeoLocation here = new GeoLocation(63, 10);
        bikeRental.addPlace(here);
        bike = new Bike();
        bikeRental.addBike(bike, here);
        me = new Person("Hallvard");
    }

    private final LocalTimeStringConverter localTimeStringConverter = new LocalTimeStringConverter();

    @FXML
    public void initialize() {
        setFromTime(LocalTime.now());
        setToTime(LocalTime.now().plus(1, ChronoUnit.HOURS));
        Platform.runLater(() -> toInput.requestFocus());
    }

    @FXML
    private TextField fromInput;

    @FXML
    private TextField toInput;

    /**
     * @return a LocalDataTime object corresponding to the from input field value
     */
    private LocalTime getFromTime() {
        return localTimeStringConverter.fromString(fromInput.getText());
    }
    /**
     * Updates the from input field value according to the LocalDateTime argument
     * @param time
     */
    private void setFromTime(final LocalTime time) {
        fromInput.setText(localTimeStringConverter.toString(time));
    }

    private LocalTime getToTime() {
        return localTimeStringConverter.fromString(toInput.getText());
    }
    private void setToTime(final LocalTime time) {
        toInput.setText(localTimeStringConverter.toString(time));
    }

    @FXML
    public void plus1HourAction() {
        setToTime(getToTime().plus(1, ChronoUnit.HOURS));
    }

    @FXML
    public void minus1HourAction() {
        setToTime(getToTime().minus(1, ChronoUnit.HOURS));
    }

    private LocalDateTime toLocalDateTime(final LocalTime time) {
        return LocalDateTime.now().withHour(time.getHour()).withMinute(time.getMinute());
    }

    @FXML
    public void rentAction() {
        bikeRental.rentBike(me, bike, toLocalDateTime(getFromTime()) , toLocalDateTime(getToTime()));
    }
}



...