Add custom aspect ratio for the projector

Change the client to accept aspect ratios like:
4:3, 16:9, 16:10 or custom over a textfield

Change server to accept aspect ratios and dropped height
This commit is contained in:
Sean Engelhardt 2019-11-22 12:25:12 +01:00
parent 4451fe979e
commit ff90f9490c
9 changed files with 195 additions and 84 deletions

View File

@ -65,7 +65,8 @@ export class Projector extends BaseModel<Projector> {
public scroll: number; public scroll: number;
public name: string; public name: string;
public width: number; public width: number;
public height: number; public aspect_ratio_numerator: number;
public aspect_ratio_denominator: number;
public reference_projector_id: number; public reference_projector_id: number;
public projectiondefaults_id: number[]; public projectiondefaults_id: number[];
public color: string; public color: string;
@ -79,6 +80,34 @@ export class Projector extends BaseModel<Projector> {
public show_title: boolean; public show_title: boolean;
public show_logo: boolean; public show_logo: boolean;
/**
* @returns Calculate the height of the projector
*/
public get height(): number {
const ratio = this.aspect_ratio_numerator / this.aspect_ratio_denominator;
return this.width / ratio;
}
/**
* get the aspect ratio as string
*/
public get aspectRatio(): string {
return [this.aspect_ratio_numerator, this.aspect_ratio_denominator].join(':');
}
/**
* Set the aspect ratio
*/
public set aspectRatio(ratioString: string) {
const ratio = ratioString.split(':').map(x => +x);
if (ratio.length === 2) {
this.aspect_ratio_numerator = ratio[0];
this.aspect_ratio_denominator = ratio[1];
} else {
throw new Error('Projector received unexpected aspect ratio! ' + ratio.toString());
}
}
public constructor(input?: any) { public constructor(input?: any) {
super(Projector.COLLECTIONSTRING, input); super(Projector.COLLECTIONSTRING, input);
} }

View File

@ -15,22 +15,40 @@
<h3 translate>Resolution and size</h3> <h3 translate>Resolution and size</h3>
<!-- Aspect ratio field --> <!-- Aspect ratio field -->
<div>
<mat-radio-group formControlName="aspectRatio" name="aspectRatio"> <mat-radio-group formControlName="aspectRatio" name="aspectRatio">
<mat-radio-button <mat-radio-button
*ngFor="let ratio of aspectRatiosKeys" *ngFor="let ratio of defaultAspectRatio"
[value]="ratio" [value]="ratio"
(change)="aspectRatioChanged($event)" (change)="onCustomAspectRatio(false)"
> >
{{ ratio }} {{ ratio }}
</mat-radio-button> </mat-radio-button>
<!-- Custom aspect ratio -->
<mat-radio-button (change)="onCustomAspectRatio(true)">
{{ 'custom' | translate }}
</mat-radio-button>
<mat-form-field *ngIf="customAspectRatio">
<input
matInput
type="text"
formControlName="aspectRatio"
[value]="previewProjector.aspectRatio"
(change)="setCustomAspectRatio()"
placeholder="{{ 'Custom aspect ratio' | translate }}"
/>
</mat-form-field>
</mat-radio-group> </mat-radio-group>
</div>
<div class="spacer-top-20 grid-form"> <div class="spacer-top-20 grid-form">
<mat-slider <mat-slider
class="grid-start" class="grid-start"
formControlName="width" formControlName="width"
[thumbLabel]="true" [thumbLabel]="true"
[min]="getMinWidth()" [min]="minWidth"
[max]="maxResolution" [max]="maxResolution"
[step]="resolutionChangeStep" [step]="resolutionChangeStep"
[value]="updateForm.value.width" [value]="updateForm.value.width"
@ -41,7 +59,7 @@
matInput matInput
type="number" type="number"
formControlName="width" formControlName="width"
[min]="getMinWidth()" [min]="minWidth"
[max]="maxResolution" [max]="maxResolution"
[step]="resolutionChangeStep" [step]="resolutionChangeStep"
[value]="updateForm.value.width" [value]="updateForm.value.width"
@ -170,7 +188,7 @@
</form> </form>
<div> <div>
<h3 translate>Preview</h3> <h3 translate>Preview</h3>
<div> <div class="preview-container">
<os-projector #preview *ngIf="previewProjector" [projector]="previewProjector"></os-projector> <os-projector #preview *ngIf="previewProjector" [projector]="previewProjector"></os-projector>
</div> </div>
</div> </div>

View File

@ -25,6 +25,7 @@ form {
.grid-start { .grid-start {
grid-column-start: 1; grid-column-start: 1;
grid-column-end: 1; grid-column-end: 1;
margin: auto 0;
width: 100%; width: 100%;
} }
@ -45,6 +46,10 @@ form {
} }
} }
.preview-container {
border: 1px solid lightgray;
}
.no-markup { .no-markup {
/* Do not let the a tag ruin the projector */ /* Do not let the a tag ruin the projector */
color: inherit; color: inherit;

View File

@ -8,7 +8,7 @@ import {
ViewEncapsulation ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef, MatRadioChange, MatSnackBar } from '@angular/material'; import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material';
import { Title } from '@angular/platform-browser'; import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -23,18 +23,6 @@ import { ClockSlideService } from '../../services/clock-slide.service';
import { ViewProjectionDefault } from '../../models/view-projection-default'; import { ViewProjectionDefault } from '../../models/view-projection-default';
import { ViewProjector } from '../../models/view-projector'; import { ViewProjector } from '../../models/view-projector';
/**
* All supported aspect rations for projectors.
*/
const aspectRatios: { [ratio: string]: number } = {
'4:3': 4 / 3,
'16:9': 16 / 9,
'16:10': 16 / 10,
'30:9': 30 / 9
};
const aspectRatio_30_9_MinWidth = 1150;
/** /**
* Dialog to edit the given projector * Dialog to edit the given projector
* Shows a preview * Shows a preview
@ -54,17 +42,17 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
@ViewChild('preview', { static: false }) @ViewChild('preview', { static: false })
public preview: ProjectorComponent; public preview: ProjectorComponent;
/**
* aspect ratios
*/
public defaultAspectRatio: string[] = ['4:3', '16:9', '16:10'];
/** /**
* The update form. Will be refreahed for each projector. Just one update * The update form. Will be refreahed for each projector. Just one update
* form can be shown per time. * form can be shown per time.
*/ */
public updateForm: FormGroup; public updateForm: FormGroup;
/**
* All aspect ratio keys/strings for the UI.
*/
public aspectRatiosKeys: string[];
/** /**
* All ProjectionDefaults to select from. * All ProjectionDefaults to select from.
*/ */
@ -80,11 +68,26 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
*/ */
public maxResolution = 2000; public maxResolution = 2000;
/**
* define the minWidth
*/
public minWidth = 800;
/** /**
* Define the step of resolution changes * Define the step of resolution changes
*/ */
public resolutionChangeStep = 10; public resolutionChangeStep = 10;
/**
* Determine to use custom aspect ratios
*/
public customAspectRatio: boolean;
/**
* regular expression to check for aspect ratio strings
*/
private aspectRatioRe = RegExp('[1-9]+[0-9]*:[1-9]+[0-9]*');
public constructor( public constructor(
title: Title, title: Title,
translate: TranslateService, translate: TranslateService,
@ -98,15 +101,18 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
private cd: ChangeDetectorRef private cd: ChangeDetectorRef
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
this.aspectRatiosKeys = Object.keys(aspectRatios);
if (projector) { if (projector) {
this.previewProjector = new Projector(projector.getModel()); this.previewProjector = new Projector(projector.getModel());
if (!this.defaultAspectRatio.some(ratio => ratio === this.previewProjector.aspectRatio)) {
this.customAspectRatio = true;
}
} }
this.updateForm = formBuilder.group({ this.updateForm = formBuilder.group({
name: ['', Validators.required], name: ['', Validators.required],
aspectRatio: ['', Validators.required], aspectRatio: ['', [Validators.required, Validators.pattern(this.aspectRatioRe)]],
width: [0, Validators.required], width: [0, Validators.required],
projectiondefaults_id: [[]], projectiondefaults_id: [[]],
clock: [true], clock: [true],
@ -143,7 +149,6 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
this.updateForm.patchValue(this.projector.projector); this.updateForm.patchValue(this.projector.projector);
this.updateForm.patchValue({ this.updateForm.patchValue({
name: this.translate.instant(this.projector.name), name: this.translate.instant(this.projector.name),
aspectRatio: this.getAspectRatioKey(),
clock: this.clockSlideService.isProjectedOn(this.projector) clock: this.clockSlideService.isProjectedOn(this.projector)
}); });
@ -174,8 +179,8 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
* Saves the current changes on the projector * Saves the current changes on the projector
*/ */
public async applyChanges(): Promise<void> { public async applyChanges(): Promise<void> {
const updateProjector: Partial<Projector> = this.updateForm.value; const updateProjector: Projector = new Projector();
updateProjector.height = this.calcHeight(this.updateForm.value.width, this.updateForm.value.aspectRatio); Object.assign(updateProjector, this.updateForm.value);
try { try {
await this.clockSlideService.setProjectedOn(this.projector, this.updateForm.value.clock); await this.clockSlideService.setProjectedOn(this.projector, this.updateForm.value.clock);
await this.repo.update(updateProjector, this.projector); await this.repo.update(updateProjector, this.projector);
@ -189,26 +194,13 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
* @param previewUpdate * @param previewUpdate
*/ */
public onChangeForm(): void { public onChangeForm(): void {
if (this.previewProjector && this.projector) { if (this.previewProjector && this.projector && this.updateForm.valid) {
Object.assign(this.previewProjector, this.updateForm.value); Object.assign(this.previewProjector, this.updateForm.value);
this.previewProjector.height = this.calcHeight(
this.updateForm.value.width,
this.updateForm.value.aspectRatio
);
this.preview.setProjector(this.previewProjector); this.preview.setProjector(this.previewProjector);
this.cd.markForCheck(); this.cd.markForCheck();
} }
} }
/**
* Helper to calc height
* @param width
* @param aspectRatio
*/
private calcHeight(width: number, aspectRatio: string): number {
return Math.round(width / aspectRatios[aspectRatio]);
}
/** /**
* Resets the given form field to the given default. * Resets the given form field to the given default.
*/ */
@ -218,41 +210,23 @@ export class ProjectorEditDialogComponent extends BaseViewComponent implements O
this.updateForm.patchValue(patchValue); this.updateForm.patchValue(patchValue);
} }
public aspectRatioChanged(event: MatRadioChange): void { /**
let width: number; * Sets the aspect Ratio to custom
if (event.value === '30:9' && this.updateForm.value.width < aspectRatio_30_9_MinWidth) { * @param event
width = aspectRatio_30_9_MinWidth; */
} else { public onCustomAspectRatio(event: boolean): void {
width = this.updateForm.value.width; this.customAspectRatio = event;
}
} }
/** /**
* Calculates the aspect ratio of the given projector. * Sets and validates custom aspect ratio values
* If no matching ratio is found, the first ratio is returned.
*
* @param projector The projector to check
* @returns the found ratio key.
*/ */
public getAspectRatioKey(): string { public setCustomAspectRatio(): void {
const ratio = this.projector.width / this.projector.height; const formRatio = this.updateForm.get('aspectRatio').value;
const RATIO_ENVIRONMENT = 0.05; const validatedRatio = formRatio.match(this.aspectRatioRe);
const foundRatioKey = Object.keys(aspectRatios).find(key => { if (validatedRatio && validatedRatio[0]) {
const value = aspectRatios[key]; const ratio = validatedRatio[0];
return value >= ratio - RATIO_ENVIRONMENT && value <= ratio + RATIO_ENVIRONMENT; this.updateForm.get('aspectRatio').setValue(ratio);
});
if (!foundRatioKey) {
return Object.keys(aspectRatios)[0];
} else {
return foundRatioKey;
}
}
public getMinWidth(): number {
if (this.updateForm.value.aspectRatio === '30:9') {
return aspectRatio_30_9_MinWidth;
} else {
return 800;
} }
} }
} }

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.6 on 2019-11-22 11:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("core", "0025_projector_color"),
]
operations = [
migrations.AddField(
model_name="projector",
name="aspect_ratio_numerator",
field=models.PositiveIntegerField(default=16),
),
migrations.AddField(
model_name="projector",
name="aspect_ratio_denominator",
field=models.PositiveIntegerField(default=9),
),
]

View File

@ -0,0 +1,45 @@
# Generated by Finn Stutzenstein on 2019-11-22 11:42
from django.db import migrations
def calculate_aspect_ratios(apps, schema_editor):
"""
Assignes every projector one aspect ratio of the ones, that OS
supported until this migration. If no matching ratio was found, the
default of 16:9 is assigned.
"""
Projector = apps.get_model("core", "Projector")
ratio_environment = 0.05
aspect_ratios = {
4 / 3: (4, 3),
16 / 9: (16, 9),
16 / 10: (16, 10),
30 / 9: (30, 9),
}
for projector in Projector.objects.all():
projector_ratio = projector.width / projector.height
ratio = (16, 9) # default, if no matching aspect ratio was found.
# Search ratio, that fits to the projector_ratio. Take first one found.
for value, _ratio in aspect_ratios.items():
if (
value >= projector_ratio - ratio_environment
and value <= projector_ratio + ratio_environment
):
ratio = _ratio
break
projector.aspect_ratio_numerator = ratio[0]
projector.aspect_ratio_denominator = ratio[1]
projector.save(skip_autoupdate=True)
class Migration(migrations.Migration):
dependencies = [
("core", "0026_projector_size_1"),
]
operations = [
migrations.RunPython(calculate_aspect_ratios),
]

View File

@ -0,0 +1,14 @@
# Generated by Finn Stutzenstein on 2019-11-22 12:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0027_projector_size_2"),
]
operations = [
migrations.RemoveField(model_name="projector", name="height",),
]

View File

@ -75,7 +75,8 @@ class Projector(RESTModelMixin, models.Model):
scroll = models.IntegerField(default=0) scroll = models.IntegerField(default=0)
width = models.PositiveIntegerField(default=1024) width = models.PositiveIntegerField(default=1024)
height = models.PositiveIntegerField(default=768) aspect_ratio_numerator = models.PositiveIntegerField(default=16)
aspect_ratio_denominator = models.PositiveIntegerField(default=9)
color = models.CharField(max_length=7, default="#000000") color = models.CharField(max_length=7, default="#000000")
background_color = models.CharField(max_length=7, default="#ffffff") background_color = models.CharField(max_length=7, default="#ffffff")

View File

@ -88,7 +88,8 @@ class ProjectorSerializer(ModelSerializer):
elements_history = JSONSerializerField(read_only=True) elements_history = JSONSerializerField(read_only=True)
width = IntegerField(min_value=800, max_value=3840, required=False) width = IntegerField(min_value=800, max_value=3840, required=False)
height = IntegerField(min_value=340, max_value=2880, required=False) aspect_ratio_numerator = IntegerField(min_value=1, required=False)
aspect_ratio_denominator = IntegerField(min_value=1, required=False)
projectiondefaults = IdPrimaryKeyRelatedField( projectiondefaults = IdPrimaryKeyRelatedField(
many=True, required=False, queryset=ProjectionDefault.objects.all() many=True, required=False, queryset=ProjectionDefault.objects.all()
@ -105,7 +106,8 @@ class ProjectorSerializer(ModelSerializer):
"scroll", "scroll",
"name", "name",
"width", "width",
"height", "aspect_ratio_numerator",
"aspect_ratio_denominator",
"reference_projector", "reference_projector",
"projectiondefaults", "projectiondefaults",
"color", "color",