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 name: string;
public width: number;
public height: number;
public aspect_ratio_numerator: number;
public aspect_ratio_denominator: number;
public reference_projector_id: number;
public projectiondefaults_id: number[];
public color: string;
@ -79,6 +80,34 @@ export class Projector extends BaseModel<Projector> {
public show_title: 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) {
super(Projector.COLLECTIONSTRING, input);
}

View File

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

View File

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

View File

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

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)
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")
background_color = models.CharField(max_length=7, default="#ffffff")

View File

@ -88,7 +88,8 @@ class ProjectorSerializer(ModelSerializer):
elements_history = JSONSerializerField(read_only=True)
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(
many=True, required=False, queryset=ProjectionDefault.objects.all()
@ -105,7 +106,8 @@ class ProjectorSerializer(ModelSerializer):
"scroll",
"name",
"width",
"height",
"aspect_ratio_numerator",
"aspect_ratio_denominator",
"reference_projector",
"projectiondefaults",
"color",