Skip to content

Commit

Permalink
fix: add area to query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
vineyardbovines committed Dec 9, 2024
1 parent 06410b5 commit 121c136
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
54 changes: 54 additions & 0 deletions src/middleware/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ interface BoundingBox {
east: number;
}

type AreaIdentifier =
| { type: "id"; id: number }
| { type: "name"; name: string }
| { type: "key"; key: string; value: string } // For IATA, ISO codes etc
| { type: "tags"; tags: AdvancedTagFilter[] };

interface AdvancedTagFilter extends TagFilter {
keyMatchStrategy?: KeyMatchStrategy;
}
Expand All @@ -32,6 +38,7 @@ interface TagFilter {

export class OverpassQueryBuilder {
private query: string[] = [];
private hasAreaSearch = false;
private options = {
timeout: 25,
maxsize: 536870912,
Expand Down Expand Up @@ -167,6 +174,47 @@ export class OverpassQueryBuilder {
return this;
}

public area(identifier: AreaIdentifier): this {
let areaFilter: string;
switch (identifier.type) {
case "id":
areaFilter = `${identifier.id}`;
break;
case "name":
areaFilter = `["name"="${this.formatTagValue(identifier.name)}"]`;
break;
case "key":
areaFilter = `["${identifier.key}"="${this.formatTagValue(identifier.value)}"]`;
break;
case "tags":
areaFilter = identifier.tags.map((filter) => this.buildTagFilter(filter)).join("");
break;
}

// Create area search followed by area assignment
this.query.push(`area${areaFilter}->.searchArea`);
this.hasAreaSearch = true;
return this;
}

nodesInArea(): this {
this.validateAreaSearch();
this.query.push("node(area:searchArea)");
return this;
}

waysInArea(): this {
this.validateAreaSearch();
this.query.push("way(area:searchArea)");
return this;
}

relationsInArea(): this {
this.validateAreaSearch();
this.query.push("relation(area:searchArea)");
return this;
}

public around(radius: number, lat: number, lon: number): this {
this.query.push(`(around:${radius},${lat},${lon})`);
return this;
Expand Down Expand Up @@ -273,4 +321,10 @@ export class OverpassQueryBuilder {
const str = num.toString();
return str.includes(".") ? str : `${str}.0`;
}

private validateAreaSearch(): void {
if (!this.hasAreaSearch) {
throw new Error("No area has been defined. Call areaQuery() first.");
}
}
}
140 changes: 140 additions & 0 deletions src/test/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,5 +304,145 @@ describe("OverpassQueryBuilder", () => {
expect(query).toContain("(51.5,-0.1,51.6,0.0)");
expect(query).toContain("out skel;");
});

it("should build complex area-based transportation query", () => {
const builder = new OverpassQueryBuilder()
.setTimeout(25)
.area({ type: "name", name: "Munich" })
.waysInArea()
.withTags([
{ key: "railway", operator: "=", value: "rail" },
{ key: "service", existence: "not_exists" },
])
.out("qt", true);

const query = builder.build();
expect(query).toContain("[timeout:25]");
expect(query).toContain('area["name"="Munich"]->.searchArea');
expect(query).toContain("way(area:searchArea)");
expect(query).toContain('["railway"="rail"][!"service"]');
expect(query).toContain("out qt body");
});
});

describe("area operations", () => {
it("should create area query by name", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "name", name: "London" })
.nodesInArea()
.out("qt");

expect(builder.build()).toContain('area["name"="London"]->.searchArea');
expect(builder.build()).toContain("node(area:searchArea)");
});

it("should create area query by IATA code", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "key", key: "iata", value: "LHR" })
.nodesInArea()
.out("qt");

expect(builder.build()).toContain('area["iata"="LHR"]->.searchArea');
expect(builder.build()).toContain("node(area:searchArea)");
});

it("should create area query by ID", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "id", id: 3600000000 })
.nodesInArea()
.out("qt");

expect(builder.build()).toContain("area3600000000->.searchArea");
expect(builder.build()).toContain("node(area:searchArea)");
});

it("should create area query with multiple tags", () => {
const builder = new OverpassQueryBuilder()
.area({
type: "tags",
tags: [
{ key: "admin_level", operator: "=", value: "8" },
{ key: "boundary", operator: "=", value: "administrative" },
],
})
.nodesInArea()
.out("qt");

expect(builder.build()).toContain(
'area["admin_level"="8"]["boundary"="administrative"]->.searchArea'
);
expect(builder.build()).toContain("node(area:searchArea)");
});

it("should support ways in area", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "name", name: "Berlin" })
.waysInArea()
.out("qt");

expect(builder.build()).toContain('area["name"="Berlin"]->.searchArea');
expect(builder.build()).toContain("way(area:searchArea)");
});

it("should support relations in area", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "name", name: "Paris" })
.relationsInArea()
.out("qt");

expect(builder.build()).toContain('area["name"="Paris"]->.searchArea');
expect(builder.build()).toContain("relation(area:searchArea)");
});

it("should handle special characters in area names", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "name", name: "São Paulo" })
.nodesInArea()
.out("qt");

expect(builder.build()).toContain('area["name"="São Paulo"]->.searchArea');
});

it("should allow tag filtering after area selection", () => {
const builder = new OverpassQueryBuilder()
.area({ type: "name", name: "London" })
.nodesInArea()
.withTag({ key: "amenity", operator: "=", value: "restaurant" })
.out("qt");

expect(builder.build()).toContain('area["name"="London"]->.searchArea');
expect(builder.build()).toContain('node(area:searchArea)["amenity"="restaurant"]');
});

it("should handle complete area-based query with multiple conditions", () => {
const builder = new OverpassQueryBuilder()
.setTimeout(30)
.area({
type: "tags",
tags: [
{ key: "admin_level", operator: "=", value: "4" },
{ key: "name", operator: "=", value: "Bavaria" },
],
})
.waysInArea()
.withTags([
{ key: "highway", operator: "=", value: "primary" },
{ key: "maxspeed", existence: "exists" },
])
.out("body");

Check failure on line 432 in src/test/query.test.ts

View workflow job for this annotation

GitHub Actions / release

Argument of type '"body"' is not assignable to parameter of type 'OutputFormat | undefined'.

const query = builder.build();
expect(query).toContain("[timeout:30]");
expect(query).toContain('area["admin_level"="4"]["name"="Bavaria"]->.searchArea');
expect(query).toContain("way(area:searchArea)");
expect(query).toContain('["highway"="primary"]["maxspeed"]');
expect(query).toContain("out body");
});

// Error cases
it("should throw error for invalid area query sequence", () => {
const builder = new OverpassQueryBuilder();
expect(() => builder.nodesInArea().out("qt").build()).toThrow("No area has been defined");
});
});
});

0 comments on commit 121c136

Please sign in to comment.