Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce placeholder missions. #221

Merged
merged 1 commit into from
Aug 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import com.bannergress.backend.dto.BannerDto;
import com.bannergress.backend.dto.BannerSettingsDto;
import com.bannergress.backend.dto.MissionDto;
import com.bannergress.backend.entities.Banner;
import com.bannergress.backend.entities.BannerSettings;
import com.bannergress.backend.entities.Mission;
import com.bannergress.backend.entities.PlaceInformation;
import com.bannergress.backend.enums.BannerListType;
import com.bannergress.backend.enums.BannerSortOrder;
Expand Down Expand Up @@ -203,6 +205,8 @@ private BannerDto toSummary(Banner banner) {
dto.id = banner.getCanonicalSlug();
dto.title = banner.getTitle();
dto.numberOfMissions = banner.getNumberOfMissions();
dto.numberOfSubmittedMissions = banner.getNumberOfSubmittedMissions();
dto.numberOfDisabledMissions = banner.getNumberOfDisabledMissions();
dto.lengthMeters = banner.getLengthMeters();
dto.startLatitude = getLatitude(banner.getStartPoint());
dto.startLongitude = getLongitude(banner.getStartPoint());
Expand All @@ -219,12 +223,16 @@ private BannerDto toSummary(Banner banner) {

private BannerDto toDetails(Banner banner) {
BannerDto dto = toSummary(banner);
dto.missions = Maps.transformValues(banner.getMissions(), MissionController::toDetails);
dto.missions = Maps.transformValues(banner.getMissionsAndPlaceholders(), this::toMissionOrPlaceholder);
dto.type = banner.getType();
dto.description = banner.getDescription();
return dto;
}

private MissionDto toMissionOrPlaceholder(Optional<Mission> input) {
return input.map(MissionController::toDetails).orElse(new MissionDto());
}

private void amendUserSettings(Principal principal, Collection<BannerDto> bannerDtos) {
if (principal != null) {
List<BannerSettings> bannerSettings = bannerSettingsService.getBannerSettings(principal.getName(),
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/bannergress/backend/dto/BannerDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ public class BannerDto {
*/
public int numberOfMissions;

/**
* Number of submitted missions.
*/
public int numberOfSubmittedMissions;

/**
* Number of disabled missions.
*/
public int numberOfDisabledMissions;

/**
* Map between the zero-based mission position and the mission. The mission
* position must be less than {@link #numberOfMissions}. The map may be sparse,
Expand Down
77 changes: 59 additions & 18 deletions src/main/java/com/bannergress/backend/entities/Banner.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.bannergress.backend.enums.BannerType;
import com.bannergress.backend.utils.PojoBuilder;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Maps;
import net.karneim.pojobuilder.GeneratePojoBuilder;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.SortNatural;
Expand Down Expand Up @@ -73,6 +75,20 @@ public class Banner {
@NotAudited
private int numberOfMissions;

/**
* Number of submitted ("not yet published") missions.
*/
@Column(name = "number_of_submitted_missions", nullable = false)
@NotAudited
private int numberOfSubmittedMissions;

/**
* Number of disabled ("not published anymore") missions.
*/
@Column(name = "number_of_disabled_missions", nullable = false)
@NotAudited
private int numberOfDisabledMissions;

/**
* Map between the zero-based mission position and the mission. The mission
* position must be less than {@link #numberOfMissions}. The map may be sparse,
Expand All @@ -86,6 +102,16 @@ public class Banner {
@SortNatural
private SortedMap<Integer, Mission> missions = new TreeMap<>();

/**
* Set of zero-based mission positions where a mission is supposed to be. This set and the keyset of {@link #missions} are mutually exclusive.
*/
@ElementCollection
@CollectionTable(name = "banner_placeholder", joinColumns = {@JoinColumn(name = "banner")})
@Column(name = "position")
@AuditJoinTable(name = "banner_placeholder_audit")
@SortNatural
private SortedSet<Integer> placeholders = new TreeSet<>();

/**
* Start portal of the first mission.
*/
Expand All @@ -101,13 +127,6 @@ public class Banner {
@NotAudited
private Integer lengthMeters;

/**
* All mission information is present.
*/
@Column(name = "complete", nullable = false)
@NotAudited
private boolean complete;

/**
* All missions are online.
*/
Expand Down Expand Up @@ -210,6 +229,22 @@ public void setNumberOfMissions(int numberOfMissions) {
this.numberOfMissions = numberOfMissions;
}

public int getNumberOfSubmittedMissions() {
return numberOfSubmittedMissions;
}

public void setNumberOfSubmittedMissions(int numberOfSubmittedMissions) {
this.numberOfSubmittedMissions = numberOfSubmittedMissions;
}

public int getNumberOfDisabledMissions() {
return numberOfDisabledMissions;
}

public void setNumberOfDisabledMissions(int numberOfDisabledMissions) {
this.numberOfDisabledMissions = numberOfDisabledMissions;
}

public SortedMap<Integer, Mission> getMissions() {
return missions;
}
Expand All @@ -218,6 +253,20 @@ public void setMissions(SortedMap<Integer, Mission> missions) {
this.missions = missions;
}

public SortedSet<Integer> getPlaceholders() {
return placeholders;
}

public void setPlaceholders(SortedSet<Integer> placeholders) {
this.placeholders = placeholders;
}

public ImmutableSortedMap<Integer, Optional<Mission>> getMissionsAndPlaceholders() {
return ImmutableSortedMap.<Integer, Optional<Mission>>naturalOrder()
.putAll(Maps.transformValues(missions, Optional::of))
.putAll(Maps.asMap(placeholders, p -> Optional.empty())).build();
}

public Point getStartPoint() {
return startPoint;
}
Expand All @@ -242,14 +291,6 @@ public void setStartPlaces(Set<Place> startPlaces) {
this.startPlaces = startPlaces;
}

public boolean isComplete() {
return complete;
}

public void setComplete(boolean complete) {
this.complete = complete;
}

public boolean isOnline() {
return online;
}
Expand Down Expand Up @@ -292,7 +333,7 @@ public boolean equals(final Object o) {
}
final Banner banner = (Banner) o;
return Objects.equals(uuid, banner.uuid) && numberOfMissions == banner.numberOfMissions
&& complete == banner.complete && online == banner.online && Objects.equals(title, banner.title)
&& online == banner.online && Objects.equals(title, banner.title)
&& Objects.equals(description, banner.description) && Objects.equals(missions, banner.missions)
&& Objects.equals(startPoint, banner.startPoint) && Objects.equals(lengthMeters, banner.lengthMeters)
&& Objects.equals(picture, banner.picture) && Objects.equals(startPlaces, banner.startPlaces)
Expand All @@ -302,7 +343,7 @@ public boolean equals(final Object o) {

@Override
public int hashCode() {
return Objects.hash(uuid, title, description, numberOfMissions, missions, startPoint, lengthMeters, complete,
online, picture, startPlaces, created, type, canonicalSlug);
return Objects.hash(uuid, title, description, numberOfMissions, missions, startPoint, lengthMeters, online,
picture, startPlaces, created, type, canonicalSlug);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.SortedMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;

Expand Down Expand Up @@ -106,6 +107,9 @@ private String hash(Banner banner) {
hasher.putInt(entry.getKey()).putUnencodedChars(entry.getValue().getPicture().toString())
.putBoolean(entry.getValue().getStatus() == MissionStatus.published);
}
for (Integer position : banner.getPlaceholders()) {
hasher.putInt(position);
}
return hasher.hash().toString();
}

Expand All @@ -122,7 +126,8 @@ private static BufferedImage loadImage(String path) {

protected byte[] createPicture(Banner banner) {
final int numberColumns = banner.getWidth();
final int numberRows = banner.getMissions().lastKey() / numberColumns + 1;
SortedMap<Integer, Optional<Mission>> missionsAndPlaceholders = banner.getMissionsAndPlaceholders();
final int numberRows = missionsAndPlaceholders.lastKey() / numberColumns + 1;
final int DISTANCE_CIRCLES = 4;
final int DIAMETER = 96;
final int MISSIONSIZE = DIAMETER + DISTANCE_CIRCLES;
Expand All @@ -136,24 +141,27 @@ protected byte[] createPicture(Banner banner) {

// loads, draws and masks the individual mission images to the banner image.
try {
threadPool.submit(() -> banner.getMissions().entrySet().parallelStream().forEach(entry -> {
BufferedImage missionImage;
Request request = new Request.Builder().url(entry.getValue().getPicture()).build();
try (Response response = client.newCall(request).execute()) {
missionImage = ImageIO.read(response.body().byteStream());
} catch (IOException ex) {
throw new RuntimeException("failed ro read image: " + entry.getValue().getPicture(), ex);
}
threadPool.submit(() -> missionsAndPlaceholders.entrySet().parallelStream().forEach(entry -> {
Optional<BufferedImage> optionalMissionImage = entry.getValue().map(mission -> {
Request request = new Request.Builder().url(mission.getPicture()).build();
try (Response response = client.newCall(request).execute()) {
return ImageIO.read(response.body().byteStream());
} catch (IOException ex) {
throw new RuntimeException("failed ro read image: " + mission.getPicture(), ex);
}
});
int missionPosition = numberColumns * numberRows - entry.getKey().intValue() - 1;
int x1 = DISTANCE_CIRCLES + (missionPosition % numberColumns) * MISSIONSIZE;
int y1 = DISTANCE_CIRCLES + (missionPosition / numberColumns) * MISSIONSIZE;
int x2 = x1 + DIAMETER;
int y2 = y1 + DIAMETER;
synchronized (graphics) {
graphics.drawImage(missionImage, x1, y1, x2, y2, 0, 0, missionImage.getWidth(),
missionImage.getHeight(), null);
BufferedImage maskImage = entry.getValue().getStatus() == MissionStatus.published ? maskImageOnline
: maskImageOffline;
optionalMissionImage.ifPresent(missionImage -> {
graphics.drawImage(missionImage, x1, y1, x2, y2, 0, 0, missionImage.getWidth(),
missionImage.getHeight(), null);
});
MissionStatus status = entry.getValue().map(Mission::getStatus).orElse(MissionStatus.submitted);
BufferedImage maskImage = status == MissionStatus.published ? maskImageOnline : maskImageOffline;
graphics.drawImage(maskImage, x1, y1, x2, y2, 0, 0, maskImage.getWidth(), maskImage.getHeight(),
null);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.bannergress.backend.services.impl;

import com.bannergress.backend.dto.BannerDto;
import com.bannergress.backend.dto.MissionDto;
import com.bannergress.backend.entities.Banner;
import com.bannergress.backend.entities.Mission;
import com.bannergress.backend.entities.MissionStep;
import com.bannergress.backend.entities.Place;
import com.bannergress.backend.enums.BannerListType;
import com.bannergress.backend.enums.BannerSortOrder;
import com.bannergress.backend.enums.MissionStatus;
import com.bannergress.backend.exceptions.MissionAlreadyUsedException;
import com.bannergress.backend.repositories.BannerRepository;
import com.bannergress.backend.repositories.BannerSpecifications;
Expand Down Expand Up @@ -198,8 +198,10 @@ private String deriveSlug(Banner banner) {

private Banner createTransient(BannerDto bannerDto, List<String> acceptableBannerSlugs)
throws MissionAlreadyUsedException {
Collection<String> missionIds = Collections2.transform(bannerDto.missions.values(),
missionDto -> missionDto.id);
Map<Integer, MissionDto> missions = Maps.filterValues(bannerDto.missions, missionDto -> missionDto.id != null);
Map<Integer, MissionDto> placeholders = Maps.filterValues(bannerDto.missions,
missionDto -> missionDto.id == null);
Collection<String> missionIds = Collections2.transform(missions.values(), missionDto -> missionDto.id);
missionService.assertNotAlreadyUsedInBanners(missionIds, acceptableBannerSlugs);
Banner banner = new Banner();
banner.setTitle(bannerDto.title);
Expand All @@ -209,7 +211,8 @@ private Banner createTransient(BannerDto bannerDto, List<String> acceptableBanne
banner.setType(bannerDto.type);
banner.getMissions().clear();
banner.getMissions()
.putAll(Maps.transformValues(bannerDto.missions, missionDto -> missionRepository.getOne(missionDto.id)));
.putAll(Maps.transformValues(missions, missionDto -> missionRepository.getOne(missionDto.id)));
banner.getPlaceholders().addAll(placeholders.keySet());
calculateData(banner);
pictureService.refresh(banner);
return banner;
Expand Down Expand Up @@ -252,13 +255,19 @@ public void deleteBySlug(String slug) {
@Override
public void calculateData(Banner banner) {
Point startPoint = null;
boolean complete = true;
boolean online = complete;
int numberOfSubmittedMissions = banner.getPlaceholders().size();
int numberOfDisabledMissions = 0;

for (Mission mission : banner.getMissions().values()) {
online &= mission.getStatus() == MissionStatus.published;
if (mission.getType() == null) {
complete = false;
switch (mission.getStatus()) {
case disabled:
numberOfDisabledMissions++;
break;
case submitted:
numberOfSubmittedMissions++;
break;
default:
break;
}
for (MissionStep step : mission.getSteps()) {
if (step.getPoi() != null) {
Expand All @@ -270,9 +279,10 @@ public void calculateData(Banner banner) {
}
}
banner.setStartPoint(startPoint);
banner.setComplete(complete);
banner.setOnline(online);
banner.setNumberOfMissions(banner.getMissions().size());
banner.setOnline(numberOfSubmittedMissions == 0 && numberOfDisabledMissions == 0);
banner.setNumberOfMissions(banner.getMissions().size() + banner.getPlaceholders().size());
banner.setNumberOfDisabledMissions(numberOfDisabledMissions);
banner.setNumberOfSubmittedMissions(numberOfSubmittedMissions);
if (startPoint != null && banner.getStartPlaces().isEmpty()) {
Collection<Place> startPlaces = placesService.getPlaces(startPoint);
banner.getStartPlaces().clear();
Expand Down
28 changes: 28 additions & 0 deletions src/main/resources/db/migration/V2_13__banner_status.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
ALTER TABLE banner DROP COLUMN complete;
ALTER TABLE banner ADD COLUMN number_of_submitted_missions integer;
ALTER TABLE banner ADD COLUMN number_of_disabled_missions integer;
UPDATE banner b SET
number_of_submitted_missions = 0,
number_of_disabled_missions = (
SELECT COUNT(*) FROM banner_mission bm
JOIN mission m ON bm.mission = m.id
WHERE bm.banner = b.uuid AND m.status = 'disabled'
);
ALTER TABLE BANNER ALTER COLUMN number_of_submitted_missions SET NOT NULL;
ALTER TABLE BANNER ALTER COLUMN number_of_disabled_missions SET NOT NULL;

CREATE TABLE banner_placeholder (
banner uuid NOT NULL,
position integer NOT NULL,
PRIMARY KEY (banner, position),
FOREIGN KEY (banner) REFERENCES banner (uuid)
);

CREATE TABLE banner_placeholder_audit (
rev integer NOT NULL,
banner uuid NOT NULL,
position integer NOT NULL,
revtype smallint,
PRIMARY KEY (rev, banner, position),
FOREIGN KEY (rev) REFERENCES revision (id)
);
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.bannergress.backend.entities.Banner;
import com.bannergress.backend.entities.Mission;
import com.bannergress.backend.enums.MissionStatus;
import org.assertj.core.util.Sets;
import org.junit.jupiter.api.Test;

import java.io.File;
Expand Down Expand Up @@ -37,6 +38,7 @@ void testCreatePicture() throws IOException {
}
banner.setMissions(missions);
banner.getMissions().get(3).setStatus(MissionStatus.disabled);
banner.setPlaceholders(Sets.newTreeSet(6, 11, 12, 17, 18, 19, 20, 21, 22, 23));

byte[] pngData = bannerPictureService.createPicture(banner);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class EntityBuilder {
.withNumberOfMissions(a($Int()))
.withMissions(sortedMapWith(0, $Mission()))
.withLengthMeters(a($Int()))
.withComplete(a($Boolean()))
.withOnline(a($Boolean()))
.withPicture(a($BannerPicture()))
.withStartPlaces(setWith($Place()))
Expand Down