refactor(annotations): remove overlap check to allow intersecting regions
Historical letter lines often intersect, so the system must support overlapping annotation regions. Removed the overlap guard from createAnnotation(), deleted ErrorCode.ANNOTATION_OVERLAP, and cleaned up all tests and frontend error mappings that referenced it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -49,8 +49,6 @@ public enum ErrorCode {
|
|||||||
// --- Annotations ---
|
// --- Annotations ---
|
||||||
/** The annotation with the given ID does not exist. 404 */
|
/** The annotation with the given ID does not exist. 404 */
|
||||||
ANNOTATION_NOT_FOUND,
|
ANNOTATION_NOT_FOUND,
|
||||||
/** The new annotation overlaps an existing one on the same page. 409 */
|
|
||||||
ANNOTATION_OVERLAP,
|
|
||||||
|
|
||||||
// --- Transcription Blocks ---
|
// --- Transcription Blocks ---
|
||||||
/** The transcription block with the given ID does not exist. 404 */
|
/** The transcription block with the given ID does not exist. 404 */
|
||||||
|
|||||||
@@ -24,15 +24,6 @@ public class AnnotationService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DocumentAnnotation createAnnotation(UUID documentId, CreateAnnotationDTO dto, UUID userId, String fileHash) {
|
public DocumentAnnotation createAnnotation(UUID documentId, CreateAnnotationDTO dto, UUID userId, String fileHash) {
|
||||||
List<DocumentAnnotation> existing =
|
|
||||||
annotationRepository.findByDocumentIdAndPageNumber(documentId, dto.getPageNumber());
|
|
||||||
|
|
||||||
boolean overlaps = existing.stream().anyMatch(a -> overlaps(a, dto));
|
|
||||||
if (overlaps) {
|
|
||||||
throw DomainException.conflict(
|
|
||||||
ErrorCode.ANNOTATION_OVERLAP, "Annotation overlaps an existing one on this page");
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentAnnotation annotation = DocumentAnnotation.builder()
|
DocumentAnnotation annotation = DocumentAnnotation.builder()
|
||||||
.documentId(documentId)
|
.documentId(documentId)
|
||||||
.pageNumber(dto.getPageNumber())
|
.pageNumber(dto.getPageNumber())
|
||||||
@@ -90,14 +81,4 @@ public class AnnotationService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── private helpers ──────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private boolean overlaps(DocumentAnnotation existing, CreateAnnotationDTO dto) {
|
|
||||||
double ex2 = existing.getX() + existing.getWidth();
|
|
||||||
double ey2 = existing.getY() + existing.getHeight();
|
|
||||||
double dx2 = dto.getX() + dto.getWidth();
|
|
||||||
double dy2 = dto.getY() + dto.getHeight();
|
|
||||||
return existing.getX() < dx2 && ex2 > dto.getX()
|
|
||||||
&& existing.getY() < dy2 && ey2 > dto.getY();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,15 +123,19 @@ class AnnotationControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
@WithMockUser(authorities = "ANNOTATE_ALL")
|
@WithMockUser(authorities = "ANNOTATE_ALL")
|
||||||
void createAnnotation_returns409_whenOverlap() throws Exception {
|
void createAnnotation_returns201_whenAnnotationsOverlap() throws Exception {
|
||||||
|
// Overlapping annotations are allowed — historical letter lines often intersect
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
DocumentAnnotation saved = DocumentAnnotation.builder()
|
||||||
|
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
||||||
|
.x(0.1).y(0.1).width(0.3).height(0.3).color("#ff0000").build();
|
||||||
when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
|
when(documentService.getDocumentById(any())).thenReturn(Document.builder().build());
|
||||||
when(annotationService.createAnnotation(any(), any(), any(), any()))
|
when(annotationService.createAnnotation(any(), any(), any(), any())).thenReturn(saved);
|
||||||
.thenThrow(DomainException.conflict(ErrorCode.ANNOTATION_OVERLAP, "Overlap"));
|
|
||||||
|
|
||||||
mockMvc.perform(post("/api/documents/" + UUID.randomUUID() + "/annotations")
|
mockMvc.perform(post("/api/documents/" + docId + "/annotations")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(ANNOTATION_JSON))
|
.content(ANNOTATION_JSON))
|
||||||
.andExpect(status().isConflict());
|
.andExpect(status().isCreated());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── DELETE /api/documents/{documentId}/annotations/{annotationId} ─────────
|
// ─── DELETE /api/documents/{documentId}/annotations/{annotationId} ─────────
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.http.HttpStatus.CONFLICT;
|
|
||||||
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
||||||
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
|
||||||
@@ -33,34 +32,13 @@ class AnnotationServiceTest {
|
|||||||
// ─── createAnnotation ─────────────────────────────────────────────────────
|
// ─── createAnnotation ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createAnnotation_throwsConflict_whenAnnotationOverlapsExisting() {
|
void createAnnotation_savesAnnotation() {
|
||||||
UUID docId = UUID.randomUUID();
|
UUID docId = UUID.randomUUID();
|
||||||
UUID userId = UUID.randomUUID();
|
UUID userId = UUID.randomUUID();
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#ff0000");
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#ff0000");
|
||||||
|
|
||||||
DocumentAnnotation existing = DocumentAnnotation.builder()
|
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
|
||||||
.x(0.2).y(0.2).width(0.3).height(0.3).color("#00ff00").build();
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1))
|
|
||||||
.thenReturn(List.of(existing));
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> annotationService.createAnnotation(docId, dto, userId, null))
|
|
||||||
.isInstanceOf(DomainException.class)
|
|
||||||
.satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(CONFLICT));
|
|
||||||
|
|
||||||
verify(annotationRepository, never()).save(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_savesAndReturns_whenNoOverlap() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID userId = UUID.randomUUID();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
|
|
||||||
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of());
|
|
||||||
DocumentAnnotation saved = DocumentAnnotation.builder()
|
DocumentAnnotation saved = DocumentAnnotation.builder()
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
||||||
.x(0.0).y(0.0).width(0.05).height(0.05).color("#ff0000").createdBy(userId).build();
|
.x(0.1).y(0.1).width(0.3).height(0.3).color("#ff0000").createdBy(userId).build();
|
||||||
when(annotationRepository.save(any())).thenReturn(saved);
|
when(annotationRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);
|
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);
|
||||||
@@ -69,6 +47,77 @@ class AnnotationServiceTest {
|
|||||||
verify(annotationRepository).save(any());
|
verify(annotationRepository).save(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createAnnotation_allowsOverlappingAnnotations() {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#ff0000");
|
||||||
|
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
// Should not throw even when overlapping annotations exist on the same page
|
||||||
|
annotationService.createAnnotation(docId, dto, userId, null);
|
||||||
|
|
||||||
|
verify(annotationRepository).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createAnnotation_setsFileHash_whenProvided() {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
|
||||||
|
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, "abc123");
|
||||||
|
|
||||||
|
assertThat(result.getFileHash()).isEqualTo("abc123");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createAnnotation_setsNullFileHash_whenNoneProvided() {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
|
||||||
|
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);
|
||||||
|
|
||||||
|
assertThat(result.getFileHash()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── createOcrAnnotation ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createOcrAnnotation_savesWithPolygon() {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
|
||||||
|
List<List<Double>> polygon = List.of(
|
||||||
|
List.of(0.1, 0.1), List.of(0.9, 0.11),
|
||||||
|
List.of(0.89, 0.14), List.of(0.11, 0.13));
|
||||||
|
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
DocumentAnnotation result = annotationService.createOcrAnnotation(
|
||||||
|
docId, dto, userId, "filehash", polygon);
|
||||||
|
|
||||||
|
assertThat(result.getPolygon()).isEqualTo(polygon);
|
||||||
|
assertThat(result.getDocumentId()).isEqualTo(docId);
|
||||||
|
verify(annotationRepository).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createOcrAnnotation_savesWithNullPolygon_whenPolygonNotProvided() {
|
||||||
|
UUID docId = UUID.randomUUID();
|
||||||
|
UUID userId = UUID.randomUUID();
|
||||||
|
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
|
||||||
|
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
DocumentAnnotation result = annotationService.createOcrAnnotation(
|
||||||
|
docId, dto, userId, "filehash", null);
|
||||||
|
|
||||||
|
assertThat(result.getPolygon()).isNull();
|
||||||
|
verify(annotationRepository).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
// ─── deleteAnnotation ─────────────────────────────────────────────────────
|
// ─── deleteAnnotation ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -118,32 +167,19 @@ class AnnotationServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createAnnotation_setsFileHash_whenProvided() {
|
void deleteAnnotation_throwsForbidden_whenUserIdIsNull() {
|
||||||
UUID docId = UUID.randomUUID();
|
UUID docId = UUID.randomUUID();
|
||||||
UUID userId = UUID.randomUUID();
|
UUID annotId = UUID.randomUUID();
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
|
UUID ownerId = UUID.randomUUID();
|
||||||
String fileHash = "abc123";
|
|
||||||
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of());
|
DocumentAnnotation annotation = DocumentAnnotation.builder()
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
.id(annotId).documentId(docId).createdBy(ownerId).build();
|
||||||
|
when(annotationRepository.findByIdAndDocumentId(annotId, docId))
|
||||||
|
.thenReturn(Optional.of(annotation));
|
||||||
|
|
||||||
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, fileHash);
|
assertThatThrownBy(() -> annotationService.deleteAnnotation(docId, annotId, null))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
assertThat(result.getFileHash()).isEqualTo(fileHash);
|
.satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_setsNullFileHash_whenNoneProvided() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID userId = UUID.randomUUID();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
|
|
||||||
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of());
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);
|
|
||||||
|
|
||||||
assertThat(result.getFileHash()).isNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── listAnnotations ──────────────────────────────────────────────────────
|
// ─── listAnnotations ──────────────────────────────────────────────────────
|
||||||
@@ -183,149 +219,4 @@ class AnnotationServiceTest {
|
|||||||
|
|
||||||
verify(annotationRepository, never()).save(any());
|
verify(annotationRepository, never()).save(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── deleteAnnotation — null userId ───────────────────────────────────────
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void deleteAnnotation_throwsForbidden_whenUserIdIsNull() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID annotId = UUID.randomUUID();
|
|
||||||
UUID ownerId = UUID.randomUUID();
|
|
||||||
|
|
||||||
DocumentAnnotation annotation = DocumentAnnotation.builder()
|
|
||||||
.id(annotId).documentId(docId).createdBy(ownerId).build();
|
|
||||||
when(annotationRepository.findByIdAndDocumentId(annotId, docId))
|
|
||||||
.thenReturn(Optional.of(annotation));
|
|
||||||
|
|
||||||
assertThatThrownBy(() -> annotationService.deleteAnnotation(docId, annotId, null))
|
|
||||||
.isInstanceOf(DomainException.class)
|
|
||||||
.satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── overlaps — partial overlap cases ────────────────────────────────────
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_noConflict_whenAnnotationIsToTheLeft() {
|
|
||||||
// existing: x=0.5, w=0.3 (x2=0.8); dto: x=0.0, w=0.4 (dx2=0.4)
|
|
||||||
// existing.getX() < dx2 → 0.5 < 0.4 → FALSE → no overlap (first && fails)
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
DocumentAnnotation existing = DocumentAnnotation.builder()
|
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
|
||||||
.x(0.5).y(0.0).width(0.3).height(0.5).color("#ff0000").build();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.4, 0.5, "#0000ff");
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of(existing));
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
annotationService.createAnnotation(docId, dto, UUID.randomUUID(), null);
|
|
||||||
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_noConflict_whenAnnotationIsToTheRight() {
|
|
||||||
// existing: x=0.0, w=0.1 (ex2=0.1); dto: x=0.2, w=0.3 (dx2=0.5)
|
|
||||||
// existing.getX() < dx2 → 0.0 < 0.5 → TRUE
|
|
||||||
// ex2 > dto.getX() → 0.1 > 0.2 → FALSE → no overlap (second && fails)
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
DocumentAnnotation existing = DocumentAnnotation.builder()
|
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
|
||||||
.x(0.0).y(0.0).width(0.1).height(0.5).color("#ff0000").build();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.2, 0.0, 0.3, 0.5, "#0000ff");
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of(existing));
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
annotationService.createAnnotation(docId, dto, UUID.randomUUID(), null);
|
|
||||||
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_noConflict_whenAnnotationIsBelow() {
|
|
||||||
// x ranges overlap, but y ranges don't
|
|
||||||
// existing: x=0.0, w=0.5, y=0.5, h=0.2 (ey2=0.7)
|
|
||||||
// dto: x=0.1, w=0.3 (dx2=0.4), y=0.0, h=0.4 (dy2=0.4)
|
|
||||||
// existing.getX() < dx2 → 0.0 < 0.4 → TRUE
|
|
||||||
// ex2 > dto.getX() → 0.5 > 0.1 → TRUE
|
|
||||||
// existing.getY() < dy2 → 0.5 < 0.4 → FALSE → no overlap (third && fails)
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
DocumentAnnotation existing = DocumentAnnotation.builder()
|
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
|
||||||
.x(0.0).y(0.5).width(0.5).height(0.2).color("#ff0000").build();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.0, 0.3, 0.4, "#0000ff");
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of(existing));
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
annotationService.createAnnotation(docId, dto, UUID.randomUUID(), null);
|
|
||||||
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── createOcrAnnotation ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createOcrAnnotation_skipsOverlapCheck_andSavesWithPolygon() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID userId = UUID.randomUUID();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
|
|
||||||
List<List<Double>> polygon = List.of(
|
|
||||||
List.of(0.1, 0.1), List.of(0.9, 0.11),
|
|
||||||
List.of(0.89, 0.14), List.of(0.11, 0.13));
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
DocumentAnnotation result = annotationService.createOcrAnnotation(
|
|
||||||
docId, dto, userId, "filehash", polygon);
|
|
||||||
|
|
||||||
assertThat(result.getPolygon()).isEqualTo(polygon);
|
|
||||||
assertThat(result.getDocumentId()).isEqualTo(docId);
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createOcrAnnotation_savesWithNullPolygon_whenPolygonNotProvided() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID userId = UUID.randomUUID();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
DocumentAnnotation result = annotationService.createOcrAnnotation(
|
|
||||||
docId, dto, userId, "filehash", null);
|
|
||||||
|
|
||||||
assertThat(result.getPolygon()).isNull();
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createOcrAnnotation_doesNotCheckOverlap_evenWhenOverlappingAnnotationExists() {
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
UUID userId = UUID.randomUUID();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#00C7B1");
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
annotationService.createOcrAnnotation(docId, dto, userId, "hash", null);
|
|
||||||
|
|
||||||
verify(annotationRepository, never()).findByDocumentIdAndPageNumber(any(), any(int.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── overlaps — partial overlap cases ────────────────────────────────────
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void createAnnotation_noConflict_whenAnnotationIsAbove() {
|
|
||||||
// x ranges overlap, y ranges don't — existing is ABOVE the new annotation
|
|
||||||
// existing: x=0.0, w=0.5, y=0.0, h=0.1 (ey2=0.1)
|
|
||||||
// dto: x=0.1, w=0.3 (dx2=0.4), y=0.2, h=0.3 (dy2=0.5)
|
|
||||||
// A: 0.0 < 0.4 → TRUE, B: 0.5 > 0.1 → TRUE, C: 0.0 < 0.5 → TRUE
|
|
||||||
// D: ey2 > dto.getY() → 0.1 > 0.2 → FALSE → no overlap (fourth && fails)
|
|
||||||
UUID docId = UUID.randomUUID();
|
|
||||||
DocumentAnnotation existing = DocumentAnnotation.builder()
|
|
||||||
.id(UUID.randomUUID()).documentId(docId).pageNumber(1)
|
|
||||||
.x(0.0).y(0.0).width(0.5).height(0.1).color("#ff0000").build();
|
|
||||||
CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.2, 0.3, 0.3, "#0000ff");
|
|
||||||
when(annotationRepository.findByDocumentIdAndPageNumber(docId, 1)).thenReturn(List.of(existing));
|
|
||||||
when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
|
||||||
|
|
||||||
annotationService.createAnnotation(docId, dto, UUID.randomUUID(), null);
|
|
||||||
|
|
||||||
verify(annotationRepository).save(any());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"error_annotation_not_found": "Die Annotation wurde nicht gefunden.",
|
"error_annotation_not_found": "Die Annotation wurde nicht gefunden.",
|
||||||
"error_annotation_overlap": "Die Annotation überschneidet sich mit einer vorhandenen.",
|
|
||||||
"annotation_outdated_notice": "Einige Annotationen beziehen sich auf eine frühere Dateiversion und werden nicht angezeigt.",
|
"annotation_outdated_notice": "Einige Annotationen beziehen sich auf eine frühere Dateiversion und werden nicht angezeigt.",
|
||||||
"error_document_not_found": "Das Dokument wurde nicht gefunden.",
|
"error_document_not_found": "Das Dokument wurde nicht gefunden.",
|
||||||
"error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.",
|
"error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"error_annotation_not_found": "Annotation not found.",
|
"error_annotation_not_found": "Annotation not found.",
|
||||||
"error_annotation_overlap": "The annotation overlaps an existing one.",
|
|
||||||
"annotation_outdated_notice": "Some annotations refer to an earlier file version and are not shown.",
|
"annotation_outdated_notice": "Some annotations refer to an earlier file version and are not shown.",
|
||||||
"error_document_not_found": "Document not found.",
|
"error_document_not_found": "Document not found.",
|
||||||
"error_document_no_file": "No file is associated with this document.",
|
"error_document_no_file": "No file is associated with this document.",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||||
"error_annotation_not_found": "Anotación no encontrada.",
|
"error_annotation_not_found": "Anotación no encontrada.",
|
||||||
"error_annotation_overlap": "La anotación se superpone con una existente.",
|
|
||||||
"annotation_outdated_notice": "Algunas anotaciones hacen referencia a una versión anterior del archivo y no se muestran.",
|
"annotation_outdated_notice": "Algunas anotaciones hacen referencia a una versión anterior del archivo y no se muestran.",
|
||||||
"error_document_not_found": "Documento no encontrado.",
|
"error_document_not_found": "Documento no encontrado.",
|
||||||
"error_document_no_file": "No hay ningún archivo asociado a este documento.",
|
"error_document_no_file": "No hay ningún archivo asociado a este documento.",
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export type ErrorCode =
|
|||||||
| 'IMPORT_ALREADY_RUNNING'
|
| 'IMPORT_ALREADY_RUNNING'
|
||||||
| 'INVALID_RESET_TOKEN'
|
| 'INVALID_RESET_TOKEN'
|
||||||
| 'ANNOTATION_NOT_FOUND'
|
| 'ANNOTATION_NOT_FOUND'
|
||||||
| 'ANNOTATION_OVERLAP'
|
|
||||||
| 'TRANSCRIPTION_BLOCK_NOT_FOUND'
|
| 'TRANSCRIPTION_BLOCK_NOT_FOUND'
|
||||||
| 'TRANSCRIPTION_BLOCK_CONFLICT'
|
| 'TRANSCRIPTION_BLOCK_CONFLICT'
|
||||||
| 'COMMENT_NOT_FOUND'
|
| 'COMMENT_NOT_FOUND'
|
||||||
@@ -82,8 +81,6 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string {
|
|||||||
return m.error_invalid_reset_token();
|
return m.error_invalid_reset_token();
|
||||||
case 'ANNOTATION_NOT_FOUND':
|
case 'ANNOTATION_NOT_FOUND':
|
||||||
return m.error_annotation_not_found();
|
return m.error_annotation_not_found();
|
||||||
case 'ANNOTATION_OVERLAP':
|
|
||||||
return m.error_annotation_overlap();
|
|
||||||
case 'TRANSCRIPTION_BLOCK_NOT_FOUND':
|
case 'TRANSCRIPTION_BLOCK_NOT_FOUND':
|
||||||
return m.error_transcription_block_not_found();
|
return m.error_transcription_block_not_found();
|
||||||
case 'TRANSCRIPTION_BLOCK_CONFLICT':
|
case 'TRANSCRIPTION_BLOCK_CONFLICT':
|
||||||
|
|||||||
Reference in New Issue
Block a user