tr-imar 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tr_imar/__init__.py ADDED
@@ -0,0 +1,118 @@
1
+ """tr_imar: Planlı Alanlar İmar Yönetmeliği ve Otopark Yönetmeliği hesapları.
2
+
3
+ Turkish zoning (imar) calculations: TAKS/KAKS, emsal dışı areas and the 30 %
4
+ cap, floor count / building height, setbacks, buildable envelope of a parcel,
5
+ parking requirement and a compliance check with Turkish messages.
6
+
7
+ Hızlı başlangıç::
8
+
9
+ from tr_imar import PlanConditions, Project, check_compliance
10
+
11
+ plan = PlanConditions(taks=0.35, kaks=1.40, floors=4, nizam="ayrik")
12
+ proje = Project(parcel_area=1000, footprint=340, floor_area=1360, floors=4,
13
+ building_height=14.4, setbacks={"on": 5, "yan": 3, "arka": 3})
14
+ print(check_compliance(proje, plan).to_text())
15
+ """
16
+
17
+ from ._version import __version__
18
+ from .compliance import CheckItem, ComplianceReport, Project, check_compliance
19
+ from .config import FIELD_SOURCES, NATIONAL, FieldSource, ImarConfig, load_config
20
+ from .density import DensityResult, resolve_density
21
+ from .emsal import (
22
+ EMSAL_DISI_CATEGORIES,
23
+ EmsalDisiItem,
24
+ EmsalDisiLine,
25
+ EmsalDisiResult,
26
+ evaluate_emsal_disi,
27
+ )
28
+ from .errors import ImarConfigError, ImarError, ImarGeometryError, ImarInputError
29
+ from .geometry import (
30
+ Envelope,
31
+ RectangleEnvelope,
32
+ buildable_envelope,
33
+ classify_edges,
34
+ is_convex,
35
+ offset_polygon,
36
+ polygon_area,
37
+ rectangle_envelope,
38
+ )
39
+ from .height import (
40
+ classify_height,
41
+ floor_height_limits,
42
+ floors_by_road_width,
43
+ floors_for_setbacks,
44
+ max_building_height,
45
+ )
46
+ from .parking import (
47
+ DEFAULT_PARKING_TABLE,
48
+ ParkingDemand,
49
+ ParkingLine,
50
+ ParkingResult,
51
+ ParkingRule,
52
+ ParkingTable,
53
+ parking_requirement,
54
+ )
55
+ from .plan import NIZAM_TYPES, ZONE_TYPES, PlanConditions
56
+ from .setbacks import SetbackRequirement, required_setbacks, tower_spacing
57
+
58
+ # Türkçe takma adlar (isteğe bağlı)
59
+ uygunluk_kontrolu = check_compliance
60
+ bahce_mesafeleri = required_setbacks
61
+ yapilasma_zarfi = buildable_envelope
62
+ otopark_ihtiyaci = parking_requirement
63
+ yogunluk = resolve_density
64
+
65
+ __all__ = [
66
+ "DEFAULT_PARKING_TABLE",
67
+ "EMSAL_DISI_CATEGORIES",
68
+ "FIELD_SOURCES",
69
+ "NATIONAL",
70
+ "NIZAM_TYPES",
71
+ "ZONE_TYPES",
72
+ "CheckItem",
73
+ "ComplianceReport",
74
+ "DensityResult",
75
+ "EmsalDisiItem",
76
+ "EmsalDisiLine",
77
+ "EmsalDisiResult",
78
+ "Envelope",
79
+ "FieldSource",
80
+ "ImarConfig",
81
+ "ImarConfigError",
82
+ "ImarError",
83
+ "ImarGeometryError",
84
+ "ImarInputError",
85
+ "ParkingDemand",
86
+ "ParkingLine",
87
+ "ParkingResult",
88
+ "ParkingRule",
89
+ "ParkingTable",
90
+ "PlanConditions",
91
+ "Project",
92
+ "RectangleEnvelope",
93
+ "SetbackRequirement",
94
+ "__version__",
95
+ "bahce_mesafeleri",
96
+ "buildable_envelope",
97
+ "check_compliance",
98
+ "classify_edges",
99
+ "classify_height",
100
+ "evaluate_emsal_disi",
101
+ "floor_height_limits",
102
+ "floors_by_road_width",
103
+ "floors_for_setbacks",
104
+ "is_convex",
105
+ "load_config",
106
+ "max_building_height",
107
+ "offset_polygon",
108
+ "otopark_ihtiyaci",
109
+ "parking_requirement",
110
+ "polygon_area",
111
+ "rectangle_envelope",
112
+ "required_setbacks",
113
+ "resolve_density",
114
+ "tower_spacing",
115
+ "uygunluk_kontrolu",
116
+ "yapilasma_zarfi",
117
+ "yogunluk",
118
+ ]
tr_imar/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__": # pragma: no cover
4
+ raise SystemExit(main())
tr_imar/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
tr_imar/cli.py ADDED
@@ -0,0 +1,537 @@
1
+ """Komut satırı arayüzü: ``tr-imar``.
2
+
3
+ Alt komutlar: ``yogunluk``, ``cekme``, ``yukseklik``, ``zarf``, ``otopark``,
4
+ ``kontrol``, ``yapilandirma``. Ayrıntı için ``tr-imar <komut> --help``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+ from typing import Any, Dict, List, Optional, Sequence, TextIO
14
+
15
+ from ._version import __version__
16
+ from .compliance import Project, check_compliance
17
+ from .config import FIELD_SOURCES, NATIONAL, ImarConfig, load_config
18
+ from .density import resolve_density
19
+ from .errors import ImarError
20
+ from .geometry import buildable_envelope, polygon_area, rectangle_envelope
21
+ from .height import (
22
+ classify_height,
23
+ floor_height_limits,
24
+ floors_by_road_width,
25
+ max_building_height,
26
+ )
27
+ from .parking import DEFAULT_PARKING_TABLE, ParkingDemand, ParkingTable, parking_requirement
28
+ from .plan import NIZAM_TYPES, ZONE_TYPES, PlanConditions
29
+ from .setbacks import required_setbacks
30
+
31
+ __all__ = ["build_parser", "main"]
32
+
33
+
34
+ def _num(x: Optional[float], unit: str = "", nd: int = 2) -> str:
35
+ if x is None:
36
+ return "-"
37
+ s = f"{x:,.{nd}f}".replace(",", "X").replace(".", ",").replace("X", ".")
38
+ return f"{s} {unit}".rstrip()
39
+
40
+
41
+ def _dump(obj: Any, out: TextIO) -> None:
42
+ out.write(json.dumps(obj, ensure_ascii=False, indent=2, default=str) + "\n")
43
+
44
+
45
+ def _config(args: argparse.Namespace) -> ImarConfig:
46
+ return load_config(args.config) if getattr(args, "config", None) else NATIONAL
47
+
48
+
49
+ def _plan_from_args(args: argparse.Namespace) -> PlanConditions:
50
+ return PlanConditions(
51
+ taks=getattr(args, "taks", None),
52
+ kaks=getattr(args, "kaks", None),
53
+ floors=getattr(args, "kat", None),
54
+ nizam=getattr(args, "nizam", "ayrik") or "ayrik",
55
+ front_setback=getattr(args, "plan_on", None),
56
+ side_setback=getattr(args, "plan_yan", None),
57
+ rear_setback=getattr(args, "plan_arka", None),
58
+ )
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Komutlar
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ def _cmd_yogunluk(args: argparse.Namespace, out: TextIO) -> int:
67
+ cfg = _config(args)
68
+ plan = _plan_from_args(args)
69
+ res = resolve_density(args.parsel, plan, envelope_area=args.zarf_alani, config=cfg)
70
+ if args.json:
71
+ _dump(res.to_dict(), out)
72
+ return 0
73
+ out.write(f"Net parsel alanı : {_num(res.parcel_area, 'm²')}\n")
74
+ out.write(
75
+ f"Azami taban alanı : {_num(res.max_footprint, 'm²')} (TAKS {res.taks:.4f}; "
76
+ f"{res.sources.get('taks', '')})\n"
77
+ )
78
+ out.write(
79
+ f"Azami emsal alanı : {_num(res.max_floor_area, 'm²')}"
80
+ + (f" (KAKS {res.kaks:.4f}; {res.sources.get('kaks', '')})" if res.kaks else "")
81
+ + "\n"
82
+ )
83
+ out.write(f"Emsal dışı %30 tavanı : {_num(res.emsal_disi_cap, 'm²')} (PAİY m.5/8)\n")
84
+ for n in res.notes:
85
+ out.write(f" * {n}\n")
86
+ return 0
87
+
88
+
89
+ def _cmd_cekme(args: argparse.Namespace, out: TextIO) -> int:
90
+ cfg = _config(args)
91
+ if args.kat is None and args.yukseklik is None:
92
+ raise ImarError("--kat veya --yukseklik verilmeli")
93
+ plan = PlanConditions(
94
+ nizam=args.nizam,
95
+ front_setback=args.plan_on,
96
+ side_setback=args.plan_yan,
97
+ rear_setback=args.plan_arka,
98
+ )
99
+ req = required_setbacks(
100
+ floors=args.kat,
101
+ height=args.yukseklik,
102
+ plan=plan,
103
+ floor_height=args.kat_yuksekligi,
104
+ config=cfg,
105
+ )
106
+ if args.json:
107
+ _dump(req.to_dict(), out)
108
+ return 0
109
+ out.write(f"Hesaba esas kat adedi : {req.floors_counted}\n")
110
+ out.write(f"Ön bahçe : {_num(req.front, 'm')} ({req.sources['front']})\n")
111
+ yan = "aranmaz (bitişik)" if req.attached_sides >= 2 else _num(req.side, "m")
112
+ out.write(f"Yan bahçe : {yan} ({req.sources['side']})\n")
113
+ out.write(f"Arka bahçe : {_num(req.rear, 'm')} ({req.sources['rear']})\n")
114
+ if req.side != req.side_park:
115
+ out.write(
116
+ f"Park alanına komşu yan/arka: {_num(req.side_park, 'm')} / "
117
+ f"{_num(req.rear_park, 'm')}\n"
118
+ )
119
+ for n in req.notes:
120
+ out.write(f" * {n}\n")
121
+ return 0
122
+
123
+
124
+ def _cmd_yukseklik(args: argparse.Namespace, out: TextIO) -> int:
125
+ cfg = _config(args)
126
+ result: Dict[str, Any] = {}
127
+ if args.yol is not None:
128
+ result["yol_genisligi"] = args.yol
129
+ result["en_cok_kat"] = floors_by_road_width(args.yol, cfg)
130
+ floors = args.kat if args.kat is not None else result.get("en_cok_kat")
131
+ if floors is not None:
132
+ lim = floor_height_limits(
133
+ args.bolge, floors=floors, ground_mezzanine=args.asma_kat, config=cfg
134
+ )
135
+ result["kat"] = floors
136
+ result["kat_yukseklikleri"] = lim
137
+ result["hmax"] = max_building_height(
138
+ floors, args.bolge, ground_mezzanine=args.asma_kat, config=cfg
139
+ )
140
+ result["hmax_subasman_dahil"] = max_building_height(
141
+ floors,
142
+ args.bolge,
143
+ ground_mezzanine=args.asma_kat,
144
+ include_subasman=True,
145
+ subasman=args.subasman,
146
+ config=cfg,
147
+ )
148
+ if args.siniflandir is not None:
149
+ result["sinif"] = classify_height(args.siniflandir, args.yapi_yuksekligi, cfg)
150
+ if not result:
151
+ raise ImarError("--kat, --yol veya --siniflandir verilmeli")
152
+ if args.json:
153
+ _dump(result, out)
154
+ return 0
155
+ if "en_cok_kat" in result:
156
+ out.write(
157
+ f"Yol genişliği {_num(args.yol, 'm')} -> en çok {result['en_cok_kat']} kat "
158
+ "(bodrum hariç; PAİY m.9/1-a)\n"
159
+ )
160
+ if "hmax" in result:
161
+ lim = result["kat_yukseklikleri"]
162
+ out.write(
163
+ f"Bölge: {args.bolge}, {result['kat']} kat; azami kat yüksekliği zemin "
164
+ f"{_num(lim['zemin'], 'm')}, normal {_num(lim['normal'], 'm')} (PAİY m.28/1)\n"
165
+ )
166
+ out.write(f"Hmax = {_num(result['hmax'], 'm')} (PAİY m.28/2)\n")
167
+ out.write(f"Hmax (subasman dahil, m.9/1-c) = {_num(result['hmax_subasman_dahil'], 'm')}\n")
168
+ if "sinif" in result:
169
+ ad = {
170
+ "normal": "yüksek yapı değil",
171
+ "yuksek": "yüksek yapı",
172
+ "cok_yuksek": "çok yüksek yapı",
173
+ }[result["sinif"]]
174
+ out.write(f"Yükseklik sınıfı: {ad} (PAİY m.4/1-aaaaa)\n")
175
+ return 0
176
+
177
+
178
+ def _parse_polygon(text: str) -> List[List[float]]:
179
+ pts: List[List[float]] = []
180
+ for part in text.replace("\n", ";").split(";"):
181
+ part = part.strip()
182
+ if not part:
183
+ continue
184
+ xy = [float(v) for v in part.replace(" ", ",").split(",") if v]
185
+ if len(xy) != 2:
186
+ raise ImarError(f"Koordinat 'x,y' biçiminde olmalı: {part!r}")
187
+ pts.append(xy)
188
+ return pts
189
+
190
+
191
+ def _read_geojson_polygon(path: str) -> List[List[float]]:
192
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
193
+ if data.get("type") == "FeatureCollection":
194
+ if not data.get("features"):
195
+ raise ImarError("GeoJSON içinde detay (feature) yok")
196
+ data = data["features"][0]
197
+ geom = data.get("geometry", data)
198
+ if geom.get("type") == "Polygon":
199
+ return [list(map(float, c[:2])) for c in geom["coordinates"][0]]
200
+ if geom.get("type") == "MultiPolygon" and len(geom["coordinates"]) == 1:
201
+ return [list(map(float, c[:2])) for c in geom["coordinates"][0][0]]
202
+ raise ImarError("GeoJSON geometrisi tek parçalı Polygon olmalı")
203
+
204
+
205
+ def _cmd_zarf(args: argparse.Namespace, out: TextIO) -> int:
206
+ cfg = _config(args)
207
+ if args.kat is None and args.yukseklik is None:
208
+ raise ImarError("--kat veya --yukseklik verilmeli")
209
+ plan = PlanConditions(
210
+ nizam=args.nizam,
211
+ front_setback=args.plan_on,
212
+ side_setback=args.plan_yan,
213
+ rear_setback=args.plan_arka,
214
+ )
215
+ req = required_setbacks(floors=args.kat, height=args.yukseklik, plan=plan, config=cfg)
216
+ if args.dikdortgen:
217
+ try:
218
+ w, d = (float(v) for v in args.dikdortgen.lower().split("x"))
219
+ except ValueError as exc:
220
+ raise ImarError("--dikdortgen CEPHExDERINLIK biçiminde olmalı, ör. 20x30") from exc
221
+ att = req.attached_sides
222
+ env = rectangle_envelope(
223
+ w,
224
+ d,
225
+ req.front,
226
+ 0.0 if att >= 1 else req.side,
227
+ req.rear,
228
+ side2=0.0 if att >= 2 else req.side,
229
+ )
230
+ res = {
231
+ "parsel_alani": w * d,
232
+ "zarf_genislik": env.width,
233
+ "zarf_derinlik": env.depth,
234
+ "zarf_alani": env.area,
235
+ "on": req.front,
236
+ "yan": req.side,
237
+ "arka": req.rear,
238
+ }
239
+ if args.json:
240
+ _dump(res, out)
241
+ else:
242
+ out.write(f"Parsel {_num(w, 'm')} x {_num(d, 'm')} = {_num(w * d, 'm²')}\n")
243
+ out.write(
244
+ f"Çekmeler: ön {_num(req.front, 'm')}, yan {_num(req.side, 'm')}"
245
+ f"{' (bitişik cephe 0)' if att else ''}, arka {_num(req.rear, 'm')}\n"
246
+ )
247
+ out.write(
248
+ f"Zarf {_num(env.width, 'm')} x {_num(env.depth, 'm')} = {_num(env.area, 'm²')}\n"
249
+ )
250
+ return 0
251
+ if args.poligon:
252
+ coords = _parse_polygon(args.poligon)
253
+ elif args.geojson:
254
+ coords = _read_geojson_polygon(args.geojson)
255
+ else:
256
+ raise ImarError("--dikdortgen, --poligon veya --geojson verilmeli")
257
+ roles = [r.strip() for r in args.roller.split(",")] if args.roller else None
258
+ if roles is None and not args.on_kenar:
259
+ raise ImarError("Poligon için --on-kenar veya --roller verilmeli")
260
+ env_p = buildable_envelope(
261
+ coords, req, edge_roles=roles, front_edges=args.on_kenar or None, method=args.yontem
262
+ )
263
+ if args.cikti:
264
+ Path(args.cikti).write_text(
265
+ json.dumps(env_p.to_geojson(), ensure_ascii=False, indent=2), encoding="utf-8"
266
+ )
267
+ if args.json:
268
+ _dump(env_p.to_geojson(), out)
269
+ return 0
270
+ out.write(f"Parsel alanı : {_num(polygon_area(coords), 'm²')}\n")
271
+ for i, (r, dist) in enumerate(zip(env_p.edge_roles, env_p.edge_distances)):
272
+ out.write(f" kenar {i}: {r:<9} {_num(dist, 'm')}\n")
273
+ out.write(
274
+ f"Zarf alanı : {_num(env_p.area, 'm²')} ({env_p.method}, {len(env_p.parts)} parça)\n"
275
+ )
276
+ for part in env_p.parts:
277
+ out.write(" " + "; ".join(f"{x:.3f},{y:.3f}" for x, y in part) + "\n")
278
+ for n in env_p.notes:
279
+ out.write(f" * {n}\n")
280
+ if args.cikti:
281
+ out.write(f"GeoJSON yazıldı: {args.cikti}\n")
282
+ return 0
283
+
284
+
285
+ def _parking_table(args: argparse.Namespace) -> ParkingTable:
286
+ if getattr(args, "tablo", None):
287
+ return ParkingTable.from_json(args.tablo)
288
+ return DEFAULT_PARKING_TABLE
289
+
290
+
291
+ def _cmd_otopark(args: argparse.Namespace, out: TextIO) -> int:
292
+ table = _parking_table(args)
293
+ if args.liste:
294
+ if args.json:
295
+ _dump(table.to_dict(), out)
296
+ return 0
297
+ group = None
298
+ for key, rule in table.items():
299
+ if rule.group != group:
300
+ group = rule.group
301
+ out.write(f"{group}\n")
302
+ if rule.kind == "mesken":
303
+ desc = rule.note
304
+ elif rule.kind == "oran":
305
+ desc = f"{rule.unit} x {rule.per:g}"
306
+ elif rule.kind in ("adet",):
307
+ desc = f"{rule.per:g} {rule.unit} için 1"
308
+ elif rule.kind == "akaryakit":
309
+ desc = "pompa ve lift/kanal başına 1"
310
+ else:
311
+ desc = f"{rule.per:g} m² için 1"
312
+ if rule.min_one_per_unit:
313
+ desc += " (bağımsız bölüm başına en az 1)"
314
+ out.write(f" {key:<18} {rule.label}: {desc}\n")
315
+ out.write(f"Kaynak: {table.source}\n")
316
+ return 0
317
+ demands: List[ParkingDemand] = []
318
+ if args.girdi:
319
+ data = json.loads(Path(args.girdi).read_text(encoding="utf-8"))
320
+ if isinstance(data, dict):
321
+ data = data.get("otopark", data.get("parking", [data]))
322
+ demands.extend(ParkingDemand.from_dict(d) for d in data)
323
+ daireler: List[float] = []
324
+ for d in args.daire or []:
325
+ daireler.extend(float(v) for v in str(d).split(",") if v.strip())
326
+ if daireler:
327
+ demands.append(ParkingDemand("mesken", unit_areas=daireler))
328
+ for spec in args.kullanim or []:
329
+ if "=" not in spec:
330
+ raise ImarError(f"--kullanim ANAHTAR=MİKTAR biçiminde olmalı: {spec!r}")
331
+ key, val = spec.split("=", 1)
332
+ rule = table[key.strip()]
333
+ q = float(val)
334
+ if rule.kind in ("adet", "oran"):
335
+ demands.append(ParkingDemand(rule.key, count=q))
336
+ elif rule.kind == "akaryakit":
337
+ demands.append(ParkingDemand(rule.key, pumps=int(q)))
338
+ else:
339
+ demands.append(ParkingDemand(rule.key, area=q))
340
+ if not demands:
341
+ raise ImarError("En az bir kullanım verilmeli (--daire, --kullanim veya --girdi)")
342
+ res = parking_requirement(
343
+ demands, table=table, factor=args.katsayi, residential_reduction=args.azaltma
344
+ )
345
+ if args.json:
346
+ _dump(res.to_dict(), out)
347
+ return 0
348
+ for line in res.lines:
349
+ out.write(f"{line.label}: {line.basis} -> {line.spaces:.2f}\n")
350
+ out.write(f"Toplam (ham) : {res.raw_total:.2f}\n")
351
+ out.write(f"Zorunlu otopark: {res.total} adet\n")
352
+ for k, v in res.extras.items():
353
+ out.write(f"Ayrıca: {v} adet {k.replace('_', ' ')} yeri\n")
354
+ for n in res.notes:
355
+ out.write(f" * {n}\n")
356
+ return 0
357
+
358
+
359
+ def _cmd_kontrol(args: argparse.Namespace, out: TextIO) -> int:
360
+ cfg = _config(args)
361
+ try:
362
+ data = json.loads(Path(args.proje).read_text(encoding="utf-8"))
363
+ except (OSError, json.JSONDecodeError) as exc:
364
+ raise ImarError(f"Proje dosyası okunamadı: {args.proje} ({exc})") from exc
365
+ if not isinstance(data, dict):
366
+ raise ImarError("Proje dosyası bir JSON nesnesi olmalı")
367
+ plan = PlanConditions.from_dict(data.get("plan", {}))
368
+ proje_data = data.get("proje", data.get("project")) or {}
369
+ proje = Project.from_dict(proje_data)
370
+ if "yapilandirma" in data:
371
+ cfg = ImarConfig.from_dict(data["yapilandirma"], base=cfg)
372
+ table = _parking_table(args)
373
+ if "otopark_tablosu" in data:
374
+ table = table.with_overrides(data["otopark_tablosu"])
375
+ report = check_compliance(proje, plan, cfg, parking_table=table, envelope_method=args.yontem)
376
+ if args.json:
377
+ out.write(report.to_json() + "\n")
378
+ else:
379
+ out.write(report.to_text() + "\n")
380
+ return 0 if report.ok else 1
381
+
382
+
383
+ def _cmd_yapilandirma(args: argparse.Namespace, out: TextIO) -> int:
384
+ cfg = _config(args)
385
+ if args.kaynaklar:
386
+ rows = {
387
+ name: {
388
+ "deger": cfg.to_dict()[name],
389
+ "dayanak": fs.article,
390
+ "degistirilemez": fs.protected,
391
+ "aciklama": fs.description,
392
+ }
393
+ for name, fs in FIELD_SOURCES.items()
394
+ }
395
+ _dump(rows, out)
396
+ else:
397
+ _dump(cfg.to_dict(), out)
398
+ return 0
399
+
400
+
401
+ # ---------------------------------------------------------------------------
402
+ # Ayrıştırıcı
403
+ # ---------------------------------------------------------------------------
404
+
405
+
406
+ def build_parser() -> argparse.ArgumentParser:
407
+ common = argparse.ArgumentParser(add_help=False)
408
+ common.add_argument(
409
+ "--config",
410
+ "--yapilandirma",
411
+ dest="config",
412
+ metavar="JSON",
413
+ help="Ulusal varsayılanları değiştiren yerel yönetmelik JSON dosyası",
414
+ )
415
+ common.add_argument("--json", action="store_true", help="Çıktıyı JSON olarak yaz")
416
+
417
+ p = argparse.ArgumentParser(
418
+ prog="tr-imar",
419
+ description="Planlı Alanlar İmar Yönetmeliği ve Otopark Yönetmeliği hesapları "
420
+ "(TAKS/KAKS, emsal dışı, bahçe mesafeleri, yapılaşma zarfı, otopark, uyum kontrolü).",
421
+ epilog="Sonuçlar ön kontrol niteliğindedir; plan notları ve idarenin yorumu esastır.",
422
+ )
423
+ p.add_argument("--version", "--surum", action="version", version=f"tr-imar {__version__}")
424
+ sub = p.add_subparsers(dest="komut", metavar="KOMUT")
425
+
426
+ s = sub.add_parser("yogunluk", parents=[common], help="Taban alanı, emsal alanı, %%30 tavanı")
427
+ s.add_argument("--parsel", type=float, required=True, help="Net imar parseli alanı (m²)")
428
+ s.add_argument("--taks", type=float, help="Plandaki TAKS")
429
+ s.add_argument("--kaks", "--emsal", dest="kaks", type=float, help="Plandaki KAKS (emsal)")
430
+ s.add_argument("--kat", type=int, help="Kat adedi (bodrum hariç)")
431
+ s.add_argument("--nizam", choices=NIZAM_TYPES, default="ayrik", help="Yapı nizamı")
432
+ s.add_argument("--zarf-alani", type=float, help="Çekmeler sonrası zarf alanı (m²)")
433
+ s.set_defaults(func=_cmd_yogunluk)
434
+
435
+ s = sub.add_parser("cekme", parents=[common], help="Asgari ön/yan/arka bahçe mesafeleri")
436
+ s.add_argument("--kat", type=int, help="Zemin üstü bodrumlar dahil kat adedi")
437
+ s.add_argument("--yukseklik", type=float, help="Görünen bina yüksekliği (m)")
438
+ s.add_argument("--nizam", choices=NIZAM_TYPES, default="ayrik")
439
+ s.add_argument("--plan-on", type=float, help="Plan notundaki ön bahçe (m)")
440
+ s.add_argument("--plan-yan", type=float, help="Plan notundaki yan bahçe (m)")
441
+ s.add_argument("--plan-arka", type=float, help="Plan notundaki arka bahçe (m)")
442
+ s.add_argument("--kat-yuksekligi", type=float, help="60.50 m üstü için tipik kat yüksekliği")
443
+ s.set_defaults(func=_cmd_cekme)
444
+
445
+ s = sub.add_parser("yukseklik", parents=[common], help="Hmax, yol genişliğine göre kat, sınıf")
446
+ s.add_argument("--kat", type=int, help="Kat adedi (bodrum hariç)")
447
+ s.add_argument("--bolge", choices=ZONE_TYPES, default="konut", help="Kat yüksekliği bölgesi")
448
+ s.add_argument("--asma-kat", action="store_true", help="Asma katlı zemin kat")
449
+ s.add_argument("--subasman", type=float, help="Subasman yüksekliği (m, en çok 1.20)")
450
+ s.add_argument("--yol", type=float, help="Ön cephedeki yolun plan genişliği (m)")
451
+ s.add_argument(
452
+ "--siniflandir",
453
+ type=float,
454
+ metavar="BINA_H",
455
+ help="Bina yüksekliğine göre yüksek yapı sınıfı",
456
+ )
457
+ s.add_argument("--yapi-yuksekligi", type=float, help="Yapı yüksekliği (m)")
458
+ s.set_defaults(func=_cmd_yukseklik)
459
+
460
+ s = sub.add_parser("zarf", parents=[common], help="Parselde yapılaşma zarfı")
461
+ g = s.add_mutually_exclusive_group()
462
+ g.add_argument("--dikdortgen", metavar="CEPHExDERINLIK", help="Dikdörtgen parsel, ör. 20x30")
463
+ g.add_argument("--poligon", metavar="'x,y;x,y;...'", help="Parsel köşeleri (metrik)")
464
+ g.add_argument("--geojson", metavar="DOSYA", help="Parsel poligonu (GeoJSON, metrik)")
465
+ s.add_argument(
466
+ "--on-kenar",
467
+ type=int,
468
+ action="append",
469
+ metavar="I",
470
+ help="Yola cepheli kenar indisi (tekrarlanabilir)",
471
+ )
472
+ s.add_argument("--roller", help="Kenar rolleri, ör. on,yan,arka,bitisik")
473
+ s.add_argument("--kat", type=int)
474
+ s.add_argument("--yukseklik", type=float)
475
+ s.add_argument("--nizam", choices=NIZAM_TYPES, default="ayrik")
476
+ s.add_argument("--plan-on", type=float)
477
+ s.add_argument("--plan-yan", type=float)
478
+ s.add_argument("--plan-arka", type=float)
479
+ s.add_argument(
480
+ "--yontem",
481
+ choices=("parallel", "exact"),
482
+ default="parallel",
483
+ help="parallel: paralel yaklaşma sınırları; exact: Öklid (shapely)",
484
+ )
485
+ s.add_argument("--cikti", metavar="DOSYA", help="Zarfı GeoJSON olarak yaz")
486
+ s.set_defaults(func=_cmd_zarf)
487
+
488
+ s = sub.add_parser("otopark", parents=[common], help="Otopark ihtiyacı (Ek-1)")
489
+ s.add_argument("--liste", action="store_true", help="Otopark tablosunu listele")
490
+ s.add_argument(
491
+ "--daire",
492
+ action="append",
493
+ metavar="M2[,M2...]",
494
+ help="Daire brüt alanları (tekrarlanabilir veya virgüllü)",
495
+ )
496
+ s.add_argument(
497
+ "--kullanim",
498
+ action="append",
499
+ metavar="ANAHTAR=MIKTAR",
500
+ help="ör. dukkan=240, otel_diger=60 (oda), spor_salonu=800 (oturma)",
501
+ )
502
+ s.add_argument("--girdi", metavar="JSON", help="Kullanım listesi JSON dosyası")
503
+ s.add_argument("--tablo", metavar="JSON", help="Tablo değişiklikleri JSON dosyası")
504
+ s.add_argument("--katsayi", type=float, default=1.0, help="Yerel artırma katsayısı")
505
+ s.add_argument(
506
+ "--azaltma", type=float, default=0.0, help="Küçük parsel konut azaltması (0-0.5)"
507
+ )
508
+ s.set_defaults(func=_cmd_otopark)
509
+
510
+ s = sub.add_parser("kontrol", parents=[common], help="Proje JSON dosyası için uyum raporu")
511
+ s.add_argument("proje", help="Proje JSON dosyası ({'plan': {...}, 'proje': {...}})")
512
+ s.add_argument("--tablo", metavar="JSON", help="Otopark tablosu değişiklikleri")
513
+ s.add_argument("--yontem", choices=("parallel", "exact"), default="parallel")
514
+ s.set_defaults(func=_cmd_kontrol)
515
+
516
+ s = sub.add_parser("yapilandirma", parents=[common], help="Eşik değerlerini JSON yaz")
517
+ s.add_argument("--kaynaklar", action="store_true", help="Dayanak maddeleriyle birlikte")
518
+ s.set_defaults(func=_cmd_yapilandirma)
519
+ return p
520
+
521
+
522
+ def main(argv: Optional[Sequence[str]] = None, out: Optional[TextIO] = None) -> int:
523
+ """CLI giriş noktası. Dönüş: 0 başarılı/uygun, 1 aykırılık var, 2 hata."""
524
+ out = out if out is not None else sys.stdout
525
+ parser = build_parser()
526
+ args = parser.parse_args(argv)
527
+ if not getattr(args, "func", None):
528
+ parser.print_help(out)
529
+ return 2
530
+ try:
531
+ return int(args.func(args, out))
532
+ except ImarError as exc:
533
+ sys.stderr.write(f"tr-imar: hata: {exc}\n")
534
+ return 2
535
+ except (OSError, ValueError) as exc:
536
+ sys.stderr.write(f"tr-imar: hata: {exc}\n")
537
+ return 2